十多年了,这个最容易犯错的Go语法终于要改了

Go 语言中你最容易犯错的语法是什么?很多人可能都有不同的答案,但是最多的答案之一就是 for 循环中变量的使用问题了。即使是 Go 团队的开发者,我也曾看到他们提交的代码犯过这种错误,更不用说其他的 Go 开发者了,比如this problem at Let’s Encrypt,几乎每个Go开发者都犯过这个错误,这种类型的错误已经在我的心里留下了阴影,每次写for循环我都心头一紧,经常使用局部变量shade一下循环变量,即使没有问题。

Russ Cox 检查了1.4万个go module,大约1.2万个github仓库,搜寻使用 x := x这种技巧来解决循环变量的问题,发现大约有600个提交都是解决这个问题的,仔细观察后,大约一半的提交是没有必要的,可能是在不准确的静态工具分析下或者对语义的混淆又或者像我一朝被蛇咬十年怕井绳的谨慎心态下做得修改。比如下面两个项目中的修改,一个是有必要的,一个没有必要:

1
2
3
4
for _, informer := range c.informerMap {
+ informer := informer
go informer.Run(stopCh)
}

另一个:

1
2
3
4
for _, a := range alarms {
+ a := a
go a.Monitor(b)
}

其中一个循环变量是接口,所以是没必要的。另一个是struct类型,并且方法的Receiver是指针类型,所以需要修改以便每次循环都是不同的指针。

所以说你看几乎相同的代码,有的是bug有的不是bug,讨不讨厌?

循环变量问题一直是一个问题,而且还不容易发现,它存在多种类型,比如:

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

或者:

1
2
3
4
5
6
7
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
for _, a := range alarms {
go a.Monitor(b)
}

或者:

1
2
3
4
5
for _, a := range alarms {
go func() {
fmt.Println(a)
}
}

或者:

1
2
3
4
for _, scheme := range artifact.Schemes {
Runtime.artifactByScheme[scheme.ID] = id
Runtime.schemesByID[scheme.ID] = scheme
}

或者:

1
2
3
for i := 0; i < n; i++ {
use(&i)
}

防不胜防。最重要的原因是当前的魂环变量ivitem等都是 per-loop的,而不是per-iteration。也就是说这些循环变量在此循环中是唯一的,而不是每迭代唯一。
解决这个问题最好的技巧就是使用局部变量(x := x)破解,比如:

1
2
3
4
for _, a := range alarms {
a := a
go a.Monitor(b)
}

大家深受其苦,所以也有一些提案希望能改进这个语法。比如#20733#24282#21130

虽然多少年Go团队对这个问题置之不理,但是今天,就在今天, Russ Cox终于出手了, 他建了一个讨论主题: #56010

因为改动这个语法,将循环变量的语义从per-loop改成per-iteration,破坏了向下兼容的许诺,所以这个改动还比较谨慎,目前还在讨论阶段,但是很显然,这是广大Gopher期望修改的一个特性。

并且Russ Cox也想到了解决方案,如果这个feature在某个版本中增加,比如 1.30,那么go module中定义的版本如果小于这个版本的库的话,则使用老的per-loop编译,大于等于这个版本使用per-iteration

你可以关注这个讨论,或者点个赞,期望这个特性早点加入到主干中。