理解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)
相关推荐
rzl025 分钟前
SpringBoot总结
spring boot·后端·firefox
小鱼人爱编程1 小时前
Java基石--反射让你直捣黄龙
前端·spring boot·后端
hqxstudying1 小时前
J2EE模式---服务层模式
java·数据库·后端·spring·oracle·java-ee
GM_8281 小时前
【最新最完整】SpringAI-1.0.0开发MCP Server,搭建MCP Client 实战笔记(进阶+详细+完整代码)
java·后端·ai编程·springai·mcp
程序员爱钓鱼1 小时前
Go语言实战案例-滑动窗口最大值
后端·google·go
Victor3562 小时前
MySQL(163) 如何理解MySQL的隔离级别?
后端
Victor3562 小时前
MySQL(164)如何设置MySQL的隔离级别?
后端
代码老y3 小时前
ASP.NET Core 高并发万字攻防战:架构设计、性能优化与生产实践
后端·性能优化·asp.net
武子康8 小时前
Java-80 深入浅出 RPC Dubbo 动态服务降级:从雪崩防护到配置中心秒级生效
java·分布式·后端·spring·微服务·rpc·dubbo
舒一笑9 小时前
我的开源项目-PandaCoder迎来史诗级大更新啦
后端·程序员·intellij idea