Go循环的坑

136 阅读1分钟

问题

最近做新需求时,需要获取资源的点赞以及阅读数,想了下可以充分使用Go的并发能力来提升效率,说干就干没几分钟就写完了对应的代码

wg := sync.WaitGroup{}
for _, v := range list {
   wg.Add(1)
   go func() {
      v.num = rpc(v.id)
      wg.Done()
   }()
}
wg.Wait()

运行了下没问题就开开心心去开发别的功能了。

第二天自己调试的时候发现了个诡异现象我打开详情页之后阅读数还是0,肯定是哪里出现了问题。

分析

在本地写了段代码进行调试

func main() {
	values := []int{1, 2, 3}
	wg := sync.WaitGroup{}
	for _, val := range values {
		wg.Add(1)
		go func() {
			fmt.Println(val)
			wg.Done()
		}()
	}
	wg.Wait()
}
// 输出结果
// 3
// 3
// 3

问题来了为啥输出都是3呢,经查询资料for-range 其实是语法糖,内部调用还是 for 循环,初始化会拷贝待遍历的列表(如 array,slice,map),然后每次遍历的v都是对同一个元素的遍历赋值。也就是说如果直接对v取地址,最终只会拿到一个地址,而对应的值就是最后遍历的那个元素所附给v的值。

对于上面代码来说主协程循环是很快就跑完的,而这个时候各个协程可能才开始跑,此时val的值已经遍历到最后一个了,所以各协程都输出了3。(如果遍历数据庞大,主协程遍历耗时较久的话,goroutine的输出会根据当时候的val的值,所以每次的输出结果不一定相同的。)

解决方案

  • 使用临时变量
for _, val := range values {
    wg.Add(1)
    val := val
    go func() {
        fmt.Println(val)
        wg.Done()
    }()
}
  • 使用闭包
for _, val := range values {
    wg.Add(1)
    go func(val int) {
        fmt.Println(val)
        wg.Done()
    }(val)
}