理解go一般原则及语义修复过程

举报
码乐 发表于 2025/04/28 21:47:08 2025/04/28
【摘要】 1 语义背景for 循环最初有关于 go vet的替代方案,让范围循环的变量在每次迭代中隐式重新定义,就像在 Dart 的循环中一样。那是 for k, v := range vals { // ... }应等效于 for k, v := range vals { k := k v := v // ... }这将使获取循环变量的地址以及在嵌套函数中捕获循环变量变得“安全”(...

1 语义背景

for 循环最初有关于 go vet的替代方案,让范围循环的变量在每次迭代中隐式重新定义,就像在 Dart 的循环中一样。

那是

	for k, v := range vals {
	  // ...
	}

应等效于

	for k, v := range vals {
	  k := k
	  v := v
	  // ...
	}

这将使获取循环变量的地址以及在嵌套函数中捕获循环变量变得“安全”(参见 #16520)。

该提案可以扩展到 vanilla 循环,尽管这会使其在语义上与其他语言不同。

我认为唯一可行的安全方法是不允许语言重新定义, go的一般性原则是不修改go的语义。

我们被当前的语义所困。这并不意味着我们不能改进它们。

例如,对于问题 20733,范围问题,我们可以更改范围循环,以便禁止获取范围参数的地址或从函数文字引用它。
这不会是重新定义;这将是一个删除。这种方法可能会消除错误,而不会意外破坏代码。

for 循环变量的作用域和生命周期在不同版本中有所变化,特别是在 Go 1.21 和 Go 1.22 之间。
以下是对这两个版本中 for 循环行为差异的说明和示例:​

2 Go 1.21 及之前的行为

在 Go 1.21 及之前的版本中,for 循环中的迭代变量(如 i)在整个循环过程中是同一个变量的复用。
这意味着在闭包中捕获该变量时,可能会导致意外的行为。​

示例:

		package main

		import "fmt"

		func main() {
		    var funcs []func()
		    for i := 0; i < 3; i++ {
		        funcs = append(funcs, func() { fmt.Println(i) })
		    }
		    for _, f := range funcs {
		        f()
		    }
		}

输出(Go 1.21):

  3
  3
  3

在此示例中,所有闭包都捕获了同一个 i 变量,循环结束后 i 的值为 3,因此所有函数都打印 3。​

  • Go 1.22 的新行为

从 Go 1.22 开始,for 循环中的迭代变量在每次迭代时都会创建一个新的变量实例。这解决了闭包中捕获变量导致的常见错误。​

相同示例在 Go 1.22 中的行为:

		package main

		import "fmt"

		func main() {
		    var funcs []func()
		    for i := 0; i < 3; i++ {
		        funcs = append(funcs, func() { fmt.Println(i) })
		    }
		    for _, f := range funcs {
		        f()
		    }
		}

输出(Go 1.22):

  0
  1
  2

现在,每个闭包都捕获了其各自迭代中的 i 值,输出符合预期。​

3 临时修改:如何在 Go 1.21 中预览临时行为

在 Go 1.21 中,可以通过设置环境变量 GOEXPERIMENT=loopvar 来预览 Go 1.22 中的循环变量行为:​

  GOEXPERIMENT=loopvar 

  go run main.go

这将使编译器在所有循环中应用新的变量作用域规则。​

4 实验性的改进1.24的问题解决

​在 Go 1.24 版本中,GOEXPERIMENT=loopvar 环境变量不再影响 for 循环变量的行为。​
从 Go 1.22 开始,循环变量的作用域行为完全由 go.mod 文件中的 go 版本声明控制。​

  • Go 1.24 中的行为

默认行为:​如果 go.mod 文件中声明的 Go 版本为 1.22 或更高(例如 go 1.24),则 for 循环变量在每次迭代时都会创建一个新的变量实例,即具有每次迭代的作用域。

旧行为:​如果 go.mod 中声明的版本低于 1.22(如 go 1.21),则循环变量在整个循环过程中是同一个变量的复用。​

因此,在 Go 1.24 中,GOEXPERIMENT=loopvar 环境变量已被弃用,不再影响循环变量的行为。​

🧪 在 Go 1.21 中的实验性支持

在 Go 1.21 中,GOEXPERIMENT=loopvar 环境变量用于启用新的循环变量行为,以便开发者提前试用。​但从 Go 1.22 开始,这一行为已成为正式特性,并通过 go.mod 控制。​

5 如何修改的?

​在 Go 1.21 和 Go 1.24.2 之间,for 循环变量的作用域和生命周期发生了显著变化,旨在解决闭包中捕获循环变量导致的常见错误。

这两个版本中 for 循环行为差异,在不修改go语义的前提下,也就是不在写 index := index, value := value 这样的显式的重赋值语句。

通过在go1.21 版本提供环境变量控制(已作废), 以及在go1.22版本隐式地重新赋值解决了该问题。

6 小结

Go 1.22 及以上版本:​循环变量的作用域行为由 go.mod 中的 go 版本声明控制,GOEXPERIMENT=loopvar 环境变量不再生效。

Go 1.21:​可以通过设置 GOEXPERIMENT=loopvar 环境变量来试用新的循环变量行为。​

建议在使用 Go 1.22 或更高版本时,通过更新 go.mod 文件中的 go 版本声明来控制循环变量的行为,而无需依赖环境变量。

兼容性控制:​新的循环变量行为仅在 go.mod 文件中指定 go 1.22 或更高版本时才会生效。这确保了旧代码的兼容性。

性能影响:​在某些情况下,新的变量创建可能会导致性能下降,特别是当循环变量是大型结构体或包含禁止复制的类型时。

工具支持:​go vet 工具在 Go 1.24 中已更新,以识别可能因循环变量作用域变化而导致的问题。

通过这些示例和说明,可以更清晰地理解go语言的for 循环行为的差异 和社区修正该问题的过程。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。