Go for 循环有时候真的很坑。。。
共 2941字,需浏览 6分钟
·
2023-02-10 20:28
每天9点准时发文 文末送福利 喜欢给个星标
送书活动 已经截止,已联系前13名同学领取奖励啦!喜不喜欢。
昨天熊哥没有放福利,今天给大家加码,6个红包封面和10元奖金,够不够丰厚?文末点击抽奖。
今天拜读一下煎鱼的文章。
不知道有多少 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
你好,我是小熊,是一个爱技术但是更爱钱的程序员。上进且佛系自律的人。喜欢发小秘密/臭屁又爱炫耀。
奋斗的大学,激情的现在。赚了钱买了房,写了书出了名。当过面试官,带过徒弟搬过砖。
大厂外来务工人员。是我,我是小熊,是不一样的烟火欢迎围观。