理解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)
相关推荐
uzong5 小时前
技术故障复盘模版
后端
GetcharZp5 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程6 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研6 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi6 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国7 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy7 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack8 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt
bobz9658 小时前
pip install 已经不再安全
后端
寻月隐君9 小时前
硬核实战:从零到一,用 Rust 和 Axum 构建高性能聊天服务后端
后端·rust·github