golang学习难点和易错点

    技术2022-07-11  91

    0.要想写出能正确运行的golang代码,有几只怪兽需要打倒(我称之为golang feature): slice, interface, panic/recover/defer, channel, goroutine, reflection, package import/management等等...后面我会详细记录打倒这些怪兽的过程,先挖坑...

    一.slice

    0.在深入理解slice之前,先回忆一下golang的数组,一般形式是a := [3]int{0,0,0},数组逻辑上一片连续的内存,因此数组由指向其第一个元素的指针ptr,数组的长度(所有已经初始化的元素数目,使用len(a)得到)和数组的大小(golang引入的概念,使用cap(a)得到)决定;由于golang的数组的每个元素都是初始化好的,因此len(a)一定等于数组的大小,也即cap(a), 因此,对于数组,我们一般只关心其长度,对数组的操作(查找或者修改元素值)一定限定在长度范围内,否则会报runtime error: index out of range;

    1.说到slice,一般形式是s := []int{0,0,0}, 与数组相比就是无需指定其大小(方括号中间不要加数字),这种初始化方式等价于:

    s := make([]int, 3)

    来看一种通用的slice初始化方式:

    s := make([]int, 5, 10) // 表示len(a) == 5, cap(a) == 10

    这表示s为长度为5(有5个元素已经初始化为0),大小为10的slice;因此,slice也是由指向其第一个元素的指针ptr,slice的len和cap决定,而与数组唯一的不一样是有些元素可以未被初始化(留给后面要讲的append()函数一丝操作的空间); 同时,与数组一样,对slice的操作(查找或者修改元素值)一定限定在长度范围内,否则会报runtime error: index out of range

    2.对slice进行切片,也即得到slice的slice,看看下面的代码:

    package main func main() { s := make([]int, 10, 20) b := s[5:] println(len(b), cap(b)) } // 运行结果为:5 15

    结合代码和运行结果,可知对slice进行切片,其返回的slice的指针将指向切口的起始元素,并且从指针位置开始,一直复用s后续的内存空间(也叫底层数组);也即b的指针是指向了s的第6个元素(s[5]),同时b复用s的初始化元素及其未初始化的元素,此时s长度为10(初始化了前10个元素), 大小为20,那么从s[6]算起,有5个元素被初始化,加上未初始化的10个元素,一共是15个元素的大小,因此len(b) == 5, cap(b) == 15

    3.其实,把slice搞得如此妖魔化的是append()函数,但是只要记住append()函数只有在slice的长度(len(s))足以容得下要append的元素数目时,其返回的new_slice的指针才会和原slice的指针相同,复用原slice的内存空间,否则返回的new_slice的指针及其内存空间和原slice不一样,那么以下的代码也不足为道:

    func strangeSlice() { a := make([]int, 1, 10) fmt.Printf("&a: %p, len: %d, cap: %d, a: %v\n", a, len(a), cap(a), a) b := append(a, 1) fmt.Printf("&b: %p, len: %d, cap: %d, b: %v\n", b, len(b), cap(b), b) _ = append(a, 2) fmt.Printf("&b: %p, len: %d, cap: %d, b: %v\n", b, len(b), cap(b), b) }

    并且对下面的运行结果能说出个所以然来:

    // 运行结果 &a: 0xc000024140, len: 1, cap: 10, a: [0] &b: 0xc000024140, len: 2, cap: 10, b: [0 1] &b: 0xc000024140, len: 2, cap: 10, b: [0 2]

    4.个人认为比较好的slice使用方式:

    // slice有具体的初始化值 s := []int{1,6,9} // slice没有具体的初始化值 s := []int{} // 或者var s []int // 后续使用append来增加元素 s = append(s, 2, 4, 8)

    总结:尽量不要使slice的长度(len)和大小(cap)不一致,同时注意如果要使用make创建slice时,当指定长度为m时,slice前m个元素会被初始化为零值(即占了前m个坑位,不过可以重新修改),使用append()函数的话是从s[m]开始追加元素

    5.slice之间元素的复制,即把源slice的元素复制到目标slice中,可以使用copy(d, s)函数

    func praCopy() { s := []int{1, 2, 3} d := make([]int, 2, 5) copy(d, s) // 当且仅当len(d) > len(s)时copy函数等价于以下代码 /* for i := range s { d[i] = s[i] } */ fmt.Printf("&d: %p, len: %d, cap: %d, d: %v\n", d, len(d), cap(d), d) }

    copy函数只会把目标slice中已初始化的元素修改为源slice中对应的元素值,而未初始化的元素不会被修改,依旧保持未初始化状态;对于上面的代码,由于d的长度为2(也即初始化了2个元素),那么即使s的长度为3,也只会把s的前2个元素复制到d中,因此运行结果为:

    &d: 0xc00001e2d0, len: 2, cap: 5, d: [1 2]

    相关参考:Golang Slice详解,Go语言slice的那些坑

    二.range

    0.先上一段代码抛砖引玉,猜猜运行结果:

    func strangeRange() { v := []int{1, 2, 3} for i := range v { v = append(v, i) } fmt.Println(v) }

    1.range的用法中,比较容易出错的就是在遍历多元素数据类型的同时要修改其中的内容,

    2.range循环在内部实现上实际就是 C 风格循环的语法糖,遍历到的值会被赋值给一个临时变量,再来一段代码:

    func strangeRange() { a := []int{1, 2, 3} // 以下代码遍历数组和遍历slice结果不一样 for i, v := range a { if i == 0 { a[1], a[2] = 200, 300 fmt.Println(a) } a[i] = v + 100 } fmt.Println(a) }

    相关参考:Go Range 内部实现 [译]

    三.值传递

    0.首先要记住:Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝(所以要特别留意内存复制的问题!!!)。因为拷贝的内容有时候是非引用类型(int、string、struct等),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等),这样就可以修改原内容数据

    1.将一个数组类型变量赋值a给另一个数组类型b,则产生一次内存复制问题:

    func praAssignment() { // 可将a改为slice看看运行结果 a := [3]int{1, 2, 3} b := a // a为数组时会发生内存拷贝, a和b的内容相同,但是内存地址不同!!! fmt.Printf("[before]&a: %p,a: %v; &b: %p, b: %v\n", &a[0], a, &b[0], b) b[0] = 6 fmt.Printf("[after]&a: %p,a: %v; &b: %p, b: %v\n", &a, a, &b, b) }

    这种写法应该尽量避免,但如果a的类型为slice,则不会有内存复制问题(此时的赋值形式和python的数组类似)

    具体可以参考:Go语言参数传递是传值还是传引用

    四.Package的管理

    先参考:拜拜了,GOPATH君!新版本Golang的包管理入门教程

    五.Nil

    先参考:理解Go语言的nil,理解Go nil

    六.reflect

    先参考:Golang的反射reflect深入理解和示例

     

    Processed: 0.012, SQL: 9