Go 团队将修改 for 循环变量的语义,Go1.21 新版本即可体验!

大家好,我是煎鱼。

之前有提到 Go for 循环变量的问题,许多面试题和泄露与此有关。

Russ Cox(下称:rsc)甚至一度表示他一直在研究这个问题,认为当前语义的代价是很大的,想看看能不能进行变更。

经过 Go1 向前兼容性和向后兼容性提案的铺垫,循环变量的这个问题将得到解决。在 Go1.21 可以进行尝试使用,预计 Go1.22 开始正式变更。

回顾问题现象

第一个例子

在 Go 语言中,我们写 for 语句时有时会出现运行和猜想的结果不一致。例如以下第一个案例的代码:

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

for _, item := range items {

 all = append(all, &item)
}
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)
}
var all []*Item

for _, item := range items {

 item := item
 all = append(all, &item)
}
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()
}
var prints []func()
for _, v := range []int{1, 2, 3} {
 prints = append(prints, func() { fmt.Println(v) })
}

for _, print := range prints {
 print()
}
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) })
}
for _, v := range []int{1, 2, 3} {
  v := v
  prints = append(prints, func() { fmt.Println(v) })
 }
for _, v := range []int{1, 2, 3} { v := v prints = append(prints, func() { fmt.Println(v) }) }

增加 v := v 语句,程序输出结果为 1,2,3。仔细翻翻你写过的 Go 工程,是不是都很熟悉?就这改造方法,赢了。

尤其是配合上 Goroutine 的写法,很多同学会更容易在此翻车。

解决方案

GOEXPERIMENT=loopvar

在 Go1.21 的新版本起,我们可以开启 GOEXPERIMENT=loopvar 来构建 Go 程序,来体验上面提到的 for 循环变量的问题。

构建命令:

GOEXPERIMENT=loopvar go install my/program
GOEXPERIMENT=loopvar go build my/program
GOEXPERIMENT=loopvar go test my/program
GOEXPERIMENT=loopvar go test my/program -bench=.
...
GOEXPERIMENT=loopvar go install my/program
GOEXPERIMENT=loopvar go build my/program
GOEXPERIMENT=loopvar go test my/program
GOEXPERIMENT=loopvar go test my/program -bench=.
...
GOEXPERIMENT=loopvar go install my/program GOEXPERIMENT=loopvar go build my/program GOEXPERIMENT=loopvar go test my/program GOEXPERIMENT=loopvar go test my/program -bench=. ...

预计在 Go1.22 起,新的 for 循环语义,将会在 go.mod 文件中的 go 行(版本声明)大于等于 Go1.22 下默认应用。

我们对应到上述的第二个例子,程序的运行结果将发生如下改变:

$ go run demo.go
3
3
3
$ GOEXPERIMENT=loopvar gotip run demo.go
1
2
3
$ go run demo.go                        
3
3
3
$ GOEXPERIMENT=loopvar gotip run demo.go
1
2
3
$ go run demo.go 3 3 3 $ GOEXPERIMENT=loopvar gotip run demo.go 1 2 3

以后就不再需要写 v := v 语句了。

模块版本控制开关

go.mod 方面,具体可以参照以下案例:

像上图的配置,Go 1.30 或更高版本将会每次迭代变量(也就是新的 for 循环语义),而早期 Go 版本的将每次循环变量,也就是 go.mod 的 Go 版本控制了新特性的语义,不同 modules 都可能会因此不一样。

如此一来上述提到的 for 循环问题都会在一定范围(版本)内被解决。

查看影响范围

可以在命令行执行以下指令进行构建:

$ go build -gcflags=all=-d=loopvar=2 cmd/go
...
modload/import.go:676:7: loop variable d now per-iteration, stack-allocated
modload/query.go:742:10: loop variable r now per-iteration, heap-allocated
$ go build -gcflags=all=-d=loopvar=2 cmd/go
...
modload/import.go:676:7: loop variable d now per-iteration, stack-allocated
modload/query.go:742:10: loop variable r now per-iteration, heap-allocated
$ go build -gcflags=all=-d=loopvar=2 cmd/go ... modload/import.go:676:7: loop variable d now per-iteration, stack-allocated modload/query.go:742:10: loop variable r now per-iteration, heap-allocated

我们就可以看到对应的文件、行数、变量。知道目前对应的是迭代还是循环,变量分配在哪里。不用靠再翻版本号再看再猜。

实际应用实验

在 2023 年 5 月初起,Google 一直在内部使用新的 for 循环的新语义。截止目前为止,没有报告任何新问题。

另外还在 Kubernetes 中尝试了新的 Go1.21 版本和 for 循环语义测试:

将 Kubernetes 从 Go 1.20 更新到 Go 1.21 时,发现了 3 个新失败的测试。而 for 循环变量的语义更改,则造成了 2 个新的失败。与普通版本更新相比,Go 官方团队认为并不是一个重大的新负担。

综合认为这不是一个大变动,且影响面可以控制。所以可变!

总结

在本次 Go 新版本更新中,Go 官方核心团队终于解决了这个十年之痛的问题。前面铺垫了真的是非常久了,这么多年,为了兼容性还出台了几个兼容性提案。真的是用心良苦!

大家要关注一下自己的应用程序,可以在 Go1.21 提前把开关开起来,看看是否有影响。如果没有影响,那就是最好的了。如果有影响,那么需要注意在后续升级新版本(Go1.22 时),要控制好 go.mod 中的 Go 版本信息。

在下个版本(Go1.21/Go1.22)起,Go 代码的 v := v 语句将会逐渐变少。可能是个好事?

面试官们也请记得修改一下你的题库了。

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blo… 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。

Go 图书系列

推荐阅读

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY0YKCGR' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片