理解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)
相关推荐
my_styles1 小时前
docker-compose部署项目(springboot服务)以及基础环境(mysql、redis等)ruoyi-ry
spring boot·redis·后端·mysql·spring cloud·docker·容器
免檒3 小时前
go语言协程调度器 GPM 模型
开发语言·后端·golang
不知道写什么的作者3 小时前
Flask快速入门和问答项目源码
后端·python·flask
caihuayuan54 小时前
生产模式下react项目报错minified react error #130的问题
java·大数据·spring boot·后端·课程设计
一只码代码的章鱼4 小时前
Spring Boot- 2 (数万字入门教程 ):数据交互篇
spring boot·后端·交互
不再幻想,脚踏实地7 小时前
Spring AOP从0到1
java·后端·spring
编程乐学(Arfan开发工程师)7 小时前
07、基础入门-SpringBoot-自动配置特性
java·spring boot·后端
会敲键盘的猕猴桃很大胆7 小时前
Day11-苍穹外卖(数据统计篇)
java·spring boot·后端·spring·信息可视化
极客智谷7 小时前
Spring Cloud动态配置刷新:@RefreshScope与@Component的协同机制解析
后端·spring·spring cloud
Lizhihao_8 小时前
Spring MVC 接口的访问方法如何设置
java·后端·spring·mvc