我们好,我是煎鱼。

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

Russ Cox(下称:rsc)乃至一度表明他一向在研究这个问题,以为当时语义的代价是很大的,想看看能不能进行改动。

通过 Go1 向前兼容性和向后兼容性提案的铺垫,循环变量的这个问题将得到处理。在 Go1.21 能够进行测验运用,预计 Go1.22 开始正式改动。

回忆问题现象

第一个比如

在 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 的写法,许多同学会更简单在此翻车。

处理方案

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=.
...

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

咱们对应到上述的第二个比如,程序的运转成果将发生如下改动:

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

以后就不再需求写 v := v 句子了。

模块版别控制开关

go.mod 方面,具体能够参照以下事例:

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

像上图的装备,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

咱们就能够看到对应的文件、行数、变量。知道现在对应的是迭代还是循环,变量分配在哪里。不必靠再翻版别号再看再猜。

实际使用试验

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

别的还在 Kubernetes 中测验了新的 Go1.21 版别和 for 循环语义测验:

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

将 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 图书系列

  • Go 言语入门系列:初探 Go 项目实战
  • Go 言语编程之旅:深化用 Go 做项目
  • Go 言语设计哲学:了解 Go 的为什么和设计思考
  • Go 言语进阶之旅:进一步深化 Go 源码

引荐阅览

  • Go1.21 速览:新内置函数 clear、min、max 和新规范库包 cmp!
  • Go1.21 速览:过了一年半,slices、maps 泛型库总算要参加规范库。。。
  • Go1.21 速览:Go 总算打算进一步支撑 WebAssembly 了。。。