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)