golang踩坑:slice传参和for range赋值和goroutine闭包

发布时间 2023-05-28 15:45:27作者: 南昌拌粉的成长

一、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启动的函数中, 把变量作为参数捕获。