GoLang基础语法学习

1. defer

defer作为GO中的一个关键字,它的作用就是延迟执行一个函数,直到当前函数执行完毕之后,defer延迟执行的函数才会执行。

非常有利于处理一些特殊的情况,例如当进行文件操作的时候,我们就可以defer对应的文件关闭操作,从而确保即使对应的代码发生问题,出错了,但是对应的文件还是可以正常关闭的,避免一些问题的出现,通常有以下用途:

  • 资源释放:比如文件关闭、数据库连接关闭、锁释放等。
  • 异常恢复:在 Go 中没有 try-catch,但可以用 defer + recover() 来处理异常。
  • 日志记录:函数结束时自动记录日志。
  • 跟踪性能:可以通过 defer 来记录某个函数的执行时间。

同时也有一些特点,例如:

延迟执行

defer

后面的函数会延迟到当前函数返回时执行。

执行顺序

如果有多个 defer

,它们会逆序执行(LIFO)。

常见用途

资源清理(如关闭文件、数据库连接、释放锁)、错误恢复、日志记录等。

性能开销

defer

有轻微的性能开销,但通常被认为是为了清晰的代码和避免资源泄漏而值得使用。

2. 指针

GO的指针可以说非常的重要,有了这个东西我们就可以避免很多的值复制,之后直接通过指针获取对应的值

指针的声明:可以通过:var ptr *int // 声明一个指向 int 类型的指针

也就是 * 专属符号,通过这个符号声明当前的变量是真么样的指针

指针的指向:

即我们的指针应该指向哪一个位置?指针,实际上指的就是一个对应数据值的地址,我们一般用:&符号来表示当前指针的位置指向,例如:

a := 1

p := &a // 获取指向a地址值的指针

获取指针指向的的具体值:

fmt.Println(*p) // 使用 *p 获取对应指针位置的值

采用符号 * 可以直接获取对应指向指定位置指针的数据值

3. 结构体

类似于JAVA当中的类了

a. 创建语法

type [structName] struct {

// 填充对应的字段属性

X ing

Y int

}

b. 初始化方式

其实就是有参构造,还是无参构造,以及创建之后结构体内部的值如何处理

有以下几种访问方式:

	v1 = Vertex{1, 2}  // 创建一个 Vertex 类型的结构体
	v2 = Vertex{X: 1}  // Y:0 被隐式地赋予零值  隐式的创建
	v3 = Vertex{}      // X:0 Y:0  给出初始化的值

c. 结构体使用方式

其实跟JAVA差不多,直接拿来用就行,但是没有了构造函数,还有对应的成员方法这一说,如下:

// 结构体的创建,就是一系列的属性的集合
// 这个其实就跟类差不多,内部有对应的成员变量
type Vertex struct {
	name string
	age int
	gender byte
}

func main() {
	fmt.Println(Vertex{"张三", 1, 0})
}

d. 字段属性的获取

GO当中,可以通过 . + 字段命的方式直接获取即可

type Vertex struct {
	X int
	Y int
}

func main() {
	v := Vertex{1, 2}
	v.X = 4
	fmt.Println(v.X)
}

4. 数组

跟JAVA当中的数组一样,但是还是需要提一些:

GO当中,数组的长度跟GO本身是写死到一起的,也就是说即使是相同类型的数组,长度不一样,那么就不是一个一样的数组

另外,GO当中,数组的长度一旦被固定,那么永远都不能被修改

a. 数组的声明方式:

对于一维数组来说:

同时还涉及到了结构体的声明,大差不差都是这样

全局:
    var arr0 [5]int = [5]int{1, 2, 3}
    var arr1 = [5]int{1, 2, 3, 4, 5}
    var arr2 = [...]int{1, 2, 3, 4, 5, 6}
    var str = [5]string{3: "hello world", 4: "tom"}
    局部:
    a := [3]int{1, 2}           // 未初始化元素值为 0。
    b := [...]int{1, 2, 3, 4}   // 通过初始化值确定数组长度。
    c := [5]int{2: 100, 4: 200} // 使用索引号初始化元素。
    d := [...]struct {
        name string
        age  uint8
    }{
        {"user1", 10}, // 可省略元素类型。
        {"user2", 20}, // 别忘了最后一行的逗号。
    }

对于二维数组来说,其实也是类似的

全局
    var arr0 [5][3]int
    var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
    局部:
    a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
    b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。

5. 切片

针对于一般的数组只能固定长度,然后进行声明,这种方式显然是不灵活的

所以就有了切片,可以动态的调整数组的大小

在 Go 语言中,切片(slice) 是一种非常重要的数据结构,它是 对数组的一个动态视图,提供了比数组更加灵活的功能。切片不仅能动态扩展长度,还能方便地引用和操作底层数组的部分数据。

但是有一点非常需要注意,切片本身是不包含任何的数据的,不占用任何的内容的,也就是说:切片就像数组的引用,并不存储任何数据,它只是描述了底层数组中的一段。

更改切片的元素会修改其底层数组中对应的元素。

和它共享底层数组的切片都会观测到这些修改。

a. 切片的声明

var s []int = []int

s := []int{1,2,4}

s := make([]int,len,cap)

主要就是这三种切片的声明方式

其中,make方法专门的就是来声明切片或者说MAP数组这种的的初始化的

其下有三个参数,一个是对应类型,例如 []int

第二个是 len() 长度,代表切片长度

第三个是cap 容量,代表对应切片容量

针对切片我们一般不对其设置长度以及cap,而是根据切实的数组去设置,所以都是0 0

/**
     * make([]T, length, capacity)
     * type: 切片的数据类型
     * length: 切片的长度,也就是实际包含的元素的数量
     * capacity: 容量 超过容量的是扩容,扩容的方式是翻倍扩容,增加为原来容量的2倍
     *           但是并不是无限制的增长,而是有一定的空间限制,如果现在的容量已经很大的前提下,就不会翻倍了
     *           而是会缩小扩大的比例,避免一直翻倍扩容
     */

另外,切片声明之后,如果对应的初始长度为0,那么这个时候直接对切片修改是会报错的,因为长度为0,没有办法对对应位置的值修改

b. 切片追加

这里的追加指的是,向切片添加原本数组可能没有的元素

官方提供了 append 方法,能够让我们向切片内加入新的元素

	var s []int
	printSlice(s)

	// 可在空切片上追加
	s = append(s, 0)
	printSlice(s)

需要注意的一点是,切片容量的扩展

//切片容量扩展
    slice := make([]int, 0, 5)
    slice = append(slice, 1, 2, 3, 4, 5)
    printSlice(slice)
    // len=5 cap=5 slice=[1 2 3 4 5]

    // 再新增
    slice = append(slice, 6)
    printSlice(slice)
    // 容量扩展了一倍,为10

一般来说,切片扩展的大小都会是原本切片的倍数,在案例中原本是5之后扩展为了10

c. 遍历方式

索引的遍历方式,可以采用 range 可以自主的遍历切片

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
	// range 两个参数,第一个为对应的索引位置,第二个为对应的值,跟MAP一样
	for i,v := range pow {
		fmt.Printf("索引为:%d,对应的值是:%d\n",i,v)
	}
}

这里可以当作是,MAP一样,有两个参数,一个是:

i 代表索引;

v 代表值

也可以忽略它,直接获取值,就用到了 _

例如

for i, _ := range pow 
for _, value := range pow

例如:
	pow := make([]int, 10)
	for i := range pow {
		// 向左边移动多少位 
		pow[i] = 1 << uint(i) // == 2**i
	}
	for _, value := range pow {
		fmt.Printf("%d\n", value)
	}

6. MAP

a. 声明以及初始化

映射的零值为 nilnil 映射既没有键,也不能添加键。

make 函数会返回给定类型的映射,并将其初始化备用。

type Vertex struct {
	Lat, Long float64
}
// MAP的声明
// 第一个代表对应的key类型,第二个代表对应的V的类型,这里是又定义了一个结构
var m map[string]Vertex

// MAP的初始化
一般都使用make对其初始化
m = make(map[string]Vertex)

// 初始化了之后就可以自主添加值了

b. 键值对的添加以及获取

在括号内写对应键,外部写值即可,如下:

m["Bell Labs"] = Vertex{
    40.68433, -74.39967,
}

// 通过值获取对应键
fmt.Println(m["Bell Labs"])

c. 删除元素

delete(m, key)

// 例如:
// 直接声明 + 初始化
	m := make(map[string]int)

	m["答案"] = 42
	fmt.Println("值:", m["答案"])

	delete(m, "答案")

d. 检测某一个键是否存在

GO是通过双值检查的,语法如下:

elem, ok = m[key]

// 检测对应的KEY是否存在,如果存在,elem会展示对应的值,不存在就为对应类型的默认值

v , ok := m["问题1"]
fmt.Println("值:", v, "是否存在?", ok)

这里分享一个strings工具类,能够把对应的字符串直接以单词划分一个string切片

7. 函数值

其实,我们在上面学函数的时候,函数不仅能作为一个方法去调用,其实也可以作为一个值,让其他函数去调用,这就是函数值,例如:

func compute(fn func(float64, float64) float64) float64 {
	return fn(3, 4)
}

func main() {
	// 定义一个函数值
	// 这里其实就是跟视图差不多,跟正常的方法的定义一样,但是怎么执行的以及哪里执行我们手动控制
	hypot := func(x, y float64) float64 {
		return math.Sqrt(x*x + y*y)
	}
	fmt.Println(hypot(5, 12))

	fmt.Println(compute(hypot))
	fmt.Println(compute(math.Pow))
}

我们可以直接在一个函数里面定义一个函数作为值,之后直接去使用,甚至函数里面也能嵌套函数的结果作为值

8. 函数闭包

为什么叫闭包呢?闭的什么?关的什么?

闭包,最大的特点就是:能够将当前函数作用范围外的各种环境,以及变量将自己结合为一体,所以叫闭包,从而形成了一个封闭的结构

  • :指的是该函数和它的作用域“封闭”在一起。函数不仅仅有自己的参数和局部变量,还可以捕获和保存外部函数的变量。
  • 包:这个词则是指它对外部环境的“封装”,也就是说,闭包将外部作用域的变量“带到”它自己的作用域中

所以,闭包实际上就是一个函数,只不过是具有了一些特殊的性质,也就是上面说的。

a. 闭包的使用

其实很简单,前面我们其实就已经学过了,函数就是闭包,只不过可能没有闭包的一些特性,如下:

func adder() func(int) int {
	sum := 0
	return func(x int) int {
		// 函数值
		addFunc := func(sum,x int) int {
			return sum + x
		}
		sum = addFunc(sum,x)
		return sum
	}
}

func main() {
	pos, neg := adder(), adder()
	for i := 0; i < 10; i++ {
		fmt.Println(
			pos(i),
			// 两个独立的函数闭包
			neg(-3*i),
		)
	}
}

在这里例子当中,我们使用的函数中包含了函数外的值,sum,即使当前函数 adder 执行完毕了,这个 sum 变量也应该被销毁了

但是!我们在内部使用了这个值,那么就会一直保留下去,我们之后调用返还的值的时候,还是能调用并且使用看似已经被销毁了的值!

b. 闭包的特性

  • 捕获外部变量:闭包能够访问并修改外部函数的局部变量,即使外部函数已经执行完毕并返回。
  • 延续生命周期:即使外部函数已经结束,闭包仍然可以访问和操作这些变量,因为它们被保存在闭包内部。

c. 作用

因为闭包的一些特性,也就是即使当前方法已经被执行完毕,但是如果其内部的一些函数引用了外部环境的一些变量,那么还是能继续使用的,一直都保存的有外部环境。

基于此,一些情况下我们就不需要声明对应的全局变量,而是局部变量就能做到一样的功效,初次之后,还有以下作用:

    • 保持状态

闭包可以让你在不使用全局变量的情况下,保持某些状态。例如,你可以利用闭包创建一个函数工厂,每次调用工厂时都可以创建一个具有某些特定初始状态的函数。

package main

import "fmt"

func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    c := counter()  // 创建闭包
    fmt.Println(c()) // 输出 1
    fmt.Println(c()) // 输出 2
    fmt.Println(c()) // 输出 3
}
  • 函数式编程

Go 是一种支持函数式编程的语言,闭包可以让我们更方便地使用函数作为参数或返回值。例如,你可以创建一个接受回调函数的函数,并在闭包中定义回调逻辑。

package main

import "fmt"

func applyOperation(x int, operation func(int) int) int {
    return operation(x)
}

func main() {
    addTwo := func(x int) int {
        return x + 2
    }
    result := applyOperation(5, addTwo)
    fmt.Println(result) // 输出 7
}

在这个例子中,applyOperation 函数接受一个 operation 闭包作为参数。我们定义了一个 addTwo 闭包,将其传递给 applyOperation 来进行操作。

  • 延迟执行和异步操作
    闭包在延迟执行和异步操作中非常有用,特别是在并发编程中,你可以利用闭包封装任务并将其传递给 goroutine 或定时器。
package main

import "fmt"
import "time"

func main() {
    for i := 0; i < 3; i++ {
        go func(i int) {
            fmt.Println("Task", i)
        }(i)  // 将 i 作为参数传递给闭包
    }
    time.Sleep(time.Second)
}

在这个例子中,go 语句启动了三个并发任务,每个任务都在闭包中执行。闭包捕获了每次循环时的 i 值,确保每个任务输出正确的结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值