理解go社区如何解决for循环赋值问题

1 语义背景

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

那是

go 复制代码
	for k, v := range vals {
	  // ...
	}

应等效于

go 复制代码
	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)在整个循环过程中是同一个变量的复用。 这意味着在闭包中捕获该变量时,可能会导致意外的行为。​

示例:

go 复制代码
		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 中的行为:

go 复制代码
		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 中的循环变量行为:​

go 复制代码
  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 循环行为的差异 和社区修正该问题的过程。

参考:

less 复制代码
[重新定义 for 循环变量语义 ·golang/go ·讨论 #56010 ·GitHub的](https://github.com/golang/go/discussions/56010)
 
 [提案: 规范: 在每次迭代中重新定义范围循环变量 ·问题 #20733 ·golang/go](https://github.com/golang/go/issues/20733)
相关推荐
Asthenia0412几秒前
为什么MySQL关联查询要“小表驱动大表”?深入解析与模拟面试复盘
后端
南雨北斗3 分钟前
分布式系统中如何保证数据一致性
后端
Asthenia04127 分钟前
Feign结构与请求链路详解及面试重点解析
后端
左灯右行的爱情10 分钟前
缓存并发更新的挑战
jvm·数据库·redis·后端·缓存
brzhang14 分钟前
告别『上线裸奔』!一文带你配齐生产级 Web 应用的 10 大核心组件
前端·后端·架构
shepherd11115 分钟前
Kafka生产环境实战经验深度总结,让你少走弯路
后端·面试·kafka
袋鱼不重28 分钟前
Cursor 最简易上手体验:谷歌浏览器插件开发3s搞定!
前端·后端·cursor
嘻嘻哈哈开森30 分钟前
Agent 系统技术分享
后端
用户40993225021231 分钟前
异步IO与Tortoise-ORM的数据库
后端·ai编程·trae
会有猫36 分钟前
LabelStudio使用阿里云OSS教程
后端