Go for 循环有时候真的很坑。。。
大家好,我是煎鱼。
不知道有多少 Go 的面试题和泄露,都和 for 循环有关。今天我在周末认真一看,发现了 redefining for loop variable semantics[1] ,看来大家踩到的坑都是一样的。
著名的硬核大佬 Russ Cox 表示他一直在研究这个问题,表示十年的经验表明了当前语义的代价是很大的,得动一动,看看能不能打破兼容性原则。
想了下之前 Go modules 的事情,我真怕他一口气就把这塔给推了...
问题
案例一
在 Go 语言中,我们写 for 语句时有时会出现运行和猜想的结果不一致。例如以下第一个案例的代码:
var all []*Item
for _, item := range items {
all = append(all, &item)
}
这段代码有问题吗?变量 all 内的 item 变量,存储进去的是什么?是每次循环的 item 值,每次都不一样,对吗?
实际上在 for 循环时,每次存入变量 all 的都是相同的 item,也就是最后一个循环的 item 值。这是 Go 面试里经常出现的题目,结合 goroutine 更风骚,毕竟还会存在乱序执行等问题。
如果你想解决这个问题,就需要把程序改写成如下:
var all []*Item
for _, item := range items {
item := item
all = append(all, &item)
}
要重新声明一个局部变量 item 变量,把 for 循环的 item 变量给存储下来,再追加进去。
案例二
接下来是第二个案例的代码:
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。这又是为什么?
问题的重点之一:关注到闭包函数,实际上所有闭包都打印的是相同的 v,也就是输出 3,原因是在 for 循环结束后,最后 v 的值被设置为了 3,仅此而已。
如果想要达到预期的效果,依然是使用万能的再赋值。改写后的代码如下:
for _, v := range []int{1, 2, 3} {
v := v
prints = append(prints, func() { fmt.Println(v) })
}
增加 v := v
语句,程序输出结果为 1,2,3。仔细翻翻你写过的 Go 工程,是不是都很熟悉?就这改造方法,赢了。
尤其是配合上 Goroutine 的写法,很多同学会更容易在此翻车。
解决方案
修复思路
实际上 Go 核心团队在内部和社区已经讨论过许久,希望重新定义 for 循环的语义。要达到的目的是:使循环变量每次迭代而不是每次循环。
解决的办法是:在每个迭代变量 x 的每个循环体开头,加一个隐式的再赋值,也就是 x := x
,就能够解决上述程序中所隐含的坑。
和我们现在做的一样,只不过我们是自己手动加的,Go 团队做的是希望在编译器内隐式处理。
让用户自己决定
比较尴尬的是 Go 团队在 Proposal: Go 2 transition[2] 中明确禁止重新定义语言的语义,所以 rsc 不能直接这么干。
因此 rsc 打算开个新坑,希望将会由用户自己决定控制这个 “破坏”,方式将会是根据每个 modules 的 go.mod 文件中的 go 行(版本声明)来决定语义。
例如,如果是在 Go1.30 对本文讨论的 for 循环将循环变量改为迭代,那么在 go.mod 文件中的 go 版本声明是将是一个关键的开关。
如下图示:
像上图的配置,Go 1.30 或更高版本将会每次迭代变量,而早期 Go 版本的将每次循环变量,也就是 go.mod 的 Go 版本控制了新特性的语义,不同 modules 都可能会因此不一样。
如此一来上述提到的 for 循环问题都会在一定范围内被解决。
总结
for 循环时的变量问题,一直是各大 Go 考官爱考的题目,也确实在实际编程 Go 代码时会遇到这类坑。
虽然 rsc 希望在 go.mod 文件上开创先河,利用 go 版本的声明,去修改语义(不允许添加和删除)。这无疑是给 Go1 兼容性保障开了一个后门。
如果实施,本次变更会导致 Go 的前后版本语义有所不同。还不如变成一个 go.mod 文件的一个语义开关,一变全变,否则这种变一些不变一些的,会给问题排查和理解上带来不少的成本。
这显然是一个很折腾人的思考题。
推荐阅读
参考资料
redefining for loop variable semantics: https://github.com/golang/go/discussions/56010
[2]Proposal: Go 2 transition: https://github.com/golang/proposal/blob/master/design/28221-go2-transitions.md#language-changes
关注和加煎鱼微信,
获取一手业内消息和知识,拉你进交流群👇
你好,我是煎鱼,出版过 Go 畅销书《Go 语言编程之旅》,再到获得 GOP(Go 领域最有观点专家)荣誉,点击蓝字查看我的出书之路。
日常分享高质量文章,输出 Go 面试、工作经验、架构设计,加微信拉读者交流群,和大家交流!