golang踩坑:slice传参,for range赋值,goroutine闭包,append
一、slice的坑
案例:
查看以下代码会输出啥?
func main() { a := []int{7,8,9} fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a) ap(a) fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a) } func ap(a []int) { a[0] = 1 a = append(a, 10) }
答案:
len: 3 cap:3 data:[7 8 9] len: 3 cap:3 data:[1 8 9]
解析:
这时ap后再输出a,会看到a[0]变成了1,但a的cap依然是3,看起来10并没有被append进去?原因很简单,Go中没有引用传递全是值传递,值传递意味着传递的是数据的拷贝,但是用的是一个地址
这实际上并不是匪夷所思,因为Go和C不一样,slice看起来像数组,实际上是一个结构体,在源码中的数据结构是:
type slice struct { array unsafe.Pointer len int cap int }
这个结构体其实也很好理解,array是一个真正的数组指针,指向一段连续内存空间的头部,len和cap代表长度和容量。
可以把ap(a)替换成ap(array: 0x123, len: 3, cap: 3),这样就比较好理解了,append修改的是数据的拷贝,但是a[0]=1修改的是地址的值
二、for range的坑
案例1:
type student struct { name string age int } func main() { m := make(map[string]*student) stus := []student{ {name: "小王子", age: 18}, {name: "娜扎", age: 23}, {name: "大王八", age: 9000}, } // 这里出现问题 for _, stu := range stus { m[stu.name] = &stu } for k, v := range m { fmt.Println(k, "=>", v.name) } }
对于该代码,我们的预期结果是:
娜扎 => 娜扎 大王八 => 大王八 小王子 => 小王子
结果是:
小王子 => 大王八 娜扎 => 大王八 大王八 => 大王八
案例2:
func main() { arr1 := []int{1, 2, 3} arr2 := make([]*int, len(arr1)) for i, v := range arr1 { arr2[i] = &v } for _, v := range arr2 { fmt.Println(*v) } }
预期输出:
1 2 3
结果输出:
3 3 3
原因解析:
因为for range在遍历值类型时,其中的v变量是一个值的拷贝,当使用&获取指针时,实际上是获取到v这个临时变量的指针,而v变量在for range中只会创建一次,之后循环中会被一直重复使用,所以在arr2赋值的时候其实都是v变量的指针,而&v最终会指向arr1最后一个元素的值拷贝
三、Goroutine中捕获参数
goroutine中捕获的循环变量, 都为循环最后的值。
func main() { for i, v := range []string{"a", "b", "c", "d", "e"} { // goroutine中捕获循环变量 go func() { fmt.Printf("index: %v, value: %v\n", i, v) }() } // 此处应该使用waitgroup实现, 为了简单使用了sleep time.Sleep(1 * time.Second) } //================输出============== index: 4, value: e index: 4, value: e index: 4, value: e index: 4, value: e index: 4, value: e
原因:
goroutine中捕获的不是"值", 而是"有地址的变量". for循环可能会先结束, 之后各个goroutine才开始执行. 因此得到的是变量的最终值。
避免方式 在goroutine启动的函数中, 把变量作为参数捕获。
func main() { for i, v := range []string{"a", "b", "c", "d", "e"} { // 把循环变量作为参数传入 go func(i int, v string) { // i, v是函数内部的局部变量 fmt.Printf("index: %v, value: %v\n", i, v) }(i, v) } time.Sleep(1 * time.Second) } //================输出============== index: 0, value: a index: 1, value: b index: 4, value: e index: 3, value: d index: 2, value: c
四、append踩坑
案例1:
package main import "fmt" type C struct { A []int } func main() { c := C{} c.B() c.C() fmt.Println(c.A) } func (receiver C) B() { receiver.A = append(receiver.A, 1) fmt.Println(receiver) } func (receiver C) C() { receiver.A = append(receiver.A, 2) fmt.Println(receiver) }
输出:
{[1]} {[2]} []
原因:receiver 是C的拷贝,所以,他的修改不影响原值,正确写法:改成指针接收者
package main import "fmt" type C struct { A []int } func main() { c := C{} c.B() c.C() fmt.Println(c.A) } func (receiver *C) B() { receiver.A = append(receiver.A, 1) } func (receiver *C) C() { receiver.A = append(receiver.A, 2) }
案例2:
package main import "fmt" func main() { i := new([]string) a(i) b(i) fmt.Println(*i) } func a(i *[]string) { c := append(*i, "a") i = &c } func b(i *[]string) { c := append(*i, "b") i = &c }
输出:[]
修改方法:i = &c