Go程序员很容易犯的错

说起容易犯的错,那还得是”循环变量”这个错误了,就连 Go 的开发者都犯过这个错误。

查看下面的go代码

func main() {
    var wg sync.WaitGroup

    values := []string{"a", "b", "c"}
    for _, v := range values {
        wg.Add(1)
        go func() {
            fmt.Println(v)
            wg.Done()
        }()
    }

    wg.Wait()
}

期望的结果值是a,b,c(可能输出的顺序不同),但是实际输出的可能是c,c,c。这是因为循环变量的作用域是整个循环,而不是单次迭代,在循环体中使用的变量是同一个变量,而不是每次迭代都是一个新的变量。

这个错误有时候隐藏很深,即使没有 goroutine,也有可能,比如下面的代码,并没有使用额外的 goroutine 和闭包,也是有问题的:

package main

import (
 "fmt"
)

type Char struct {
 Char *string
}

func main() {
 var chars []Char

 values := []string{"a", "b", "c"}
 for _, v := range values {
  chars = append(chars, Char{Char: &v})
 }

 for _, v := range chars {
  fmt.Println(*v.Char)
 }
}

输出也大概率是ccc,因为给每个Char的字段赋值的是 v 的指针,v 在整个循环中都是一个变量,所以最后的结果都是c

我在这里摔了好多跤,以至于我写 for 循环的时候都战战兢兢的,不管有无必要,很多时候我都是先把循环变量赋值给一个局部变量,然后再使用。
比如下面的代码:

func main() {
  var sync sync.WaitGroup

  nums := []int{"a", "b", "c"}
  for _, v:= range nums {
    sync.Add(1)
    v := v  //加一个临时变量
    go func() {
     fmt.println(v)
     sync.Done()
    }()
  }

  sync.Wait()
}

如果你使用 Go 1.21, 你可以开始这个功能,使用GOEXPERIMENT=loopvar go run main.go运行上面的程序,会输出cba这样的输出,不再是ccc了。这个特性在 Go 1.22 中会默认开启,不需要设置GOEXPERIMENT了。还有一两个月才能正式发布 go 1.22,大家可以使用 gotip 测试:

$ gotip run main.go

不只是for-range,下面的3-clause也是同样的问题:

unc main() {
 var ids []*int
 for i := 0; i < 3; i++ {
  i = 10
  ids = append(ids, &i)
 }

 for _, id := range ids {
  fmt.Println(*id)
 }
}

按照正常的结果,i的结果应该是10,但是在go1.22之前输出的11。在append的时候,追加的是i的指针,是同一个i变量地址。在go1.22以后go修复了这个问题,输出结果是10,每次迭代的时候,都会创建一个新的变量,所以ids中的元素都是指向不同的变量,而不是同一个变量。

go确实简单,容易上手,这是事实。不过简单背后的是复杂被隐藏起来了,在真实场景中使用时,很多细节还是需要多注意,在实践中踩过坑才会深有体会~

参考资料

mp.weixin.qq.com/s/ReAUdcByet0_hfK...
go.dev/doc/faq#closures_and_gorout...
github.com/golang/go/issues/60078
go.googlesource.com/proposal/+/mas...

go
本作品采用《CC 协议》,转载必须注明作者和本文链接
fengzi
讨论数量: 4

这个原因是闭包

4个月前 评论
编程爱好者 (楼主) 4个月前

一般都是直接通过key去slice读需要的元素

4个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!