前言

前段时间看到了一个提案,是关于 go for 循环的一个提案,根据提案看到了去年 rsc 在社区发出的讨论,讨论的内容主要是为了解决 for 循环变量的问题,是什么样的问题呢,常见的例子如下:

1
2
3
4
var all []*Item
for _, item := range items {
	all = append(all, &item)
}

这段代码有一个问题,循环结束后,all 的内容是包含了 len(all) 个相同的指针,指针指向迭代的最后一个 item。为什么会发生这种情况呢,因为 item 变量是每个循环的而不是每次迭代的,&item每次迭代都是相同的,并且每次迭代都会被覆盖。 怎么解决呢,最简单的方法就是添加 item := item 这段代码:

1
2
3
4
5
var all []*Item
for _, item := range items {
	item := item
	all = append(all, &item)
}

我在使用 Goroutine 协程时也经常遇到这种问题。这种错误已导致许多公司出现生产问题,包括 Lets Encrypt 公开记录的问题。

go 社区为了解决这个问题,打算将循环变量改为每次迭代,即隐式的添加上面的代码。由于其它一些原因,直到今年的6月才正式决定在 go 1.21 中添加 GOEXPERIMENT=loopvar 进行相应的尝试,并且将在 go1.22 版本中正式推出。 为了确保与现有代码的向后兼容性,新语义将仅适用于声明go 1.22或稍后在其go.mod文件中声明的模块中包含的包。

特性测试

我们通过不同版本运行的结果来对比

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

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

没使用旧版本运行的结果是:

1
2
3
3
3
3

使用 gotip 运行的结果如下:

1
2
3
1
2
3

参考