几乎世界上每个 Golang 程序员都踩过一遍 for 循环变量的坑

举栗子🌰

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

	for _, item := range ids {
		println(*item)
	}
}

可以在go.dev/play中运行

答案是:打印出来的全是 10。

这个结果实在离谱。原因是因为在目前 Go 的设计中,for 中循环变量的定义是 per loop 而非 per iteration。也就是整个 for 循环期间,变量 i 只会有一个。以上代码等价于:

var ids []*int

var i int
// i实例化以后都是同一个内存地址
for i = 0; i < 10; i++ {
	ids = append(ids, &i)
}

同样的问题在闭包使用循环变量时也存在,代码如下:

var prints []func()

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

for _, print := range prints {
    print()
}

根据上面的经验,闭包 func 中 fmt.Println(v),捕获到的 v 都是同一个变量。因此打印出来的都是 3。

解决方法

引用局部变量

在目前的 go 版本中,正常来说我们会这么解决:

var ids []*int

for i := 0; i < 10; i++ {
	i := i // 局部变量
	ids = append(ids, &i)
}

定义一个新的局部变量, 这样无论闭包还是指针,每次迭代时所引用的内存都不一样了。

官方修复

目前使用go.1.22+版本已修复