这个问题在 Go 1.22 版本前后有完全不同的答案。
核心结论
- Go 1.22 之前:不会变化。
for range的迭代变量是复用的,整个循环过程中,迭代变量的内存地址始终固定。 - Go 1.22 及之后:会变化。为了减少常见的闭包捕获错误,Go 1.22 修改了语义,每次迭代都会创建一个新的变量。
1. 经典陷阱 (Go < 1.22)
在 Go 1.22 之前,由于迭代变量被复用,导致了两个经典的“坑”:取地址问题和闭包捕获问题。
现象一:取地址得到相同值
如果你在循环中获取迭代变量的地址并存入切片,最终切片中的所有指针都会指向同一个地址,存储的值往往是最后一次循环的元素值。
// Go 1.21 及之前
arr := []int{1, 2, 3}
var ptrs []*int
for _, v := range arr {
// 错误:v 是复用的,&v 在整个循环中是同一个地址
ptrs = append(ptrs, &v)
}
// 输出全是 3,而不是 1, 2, 3
for _, p := range ptrs {
fmt.Println(*p)
}现象二:Goroutine 闭包捕获错误
在循环中启动 Goroutine 并直接引用迭代变量,由于 Goroutine 执行的滞后性,当它运行时,循环可能已经结束,此时它看到的变量值是最后一次迭代的值。
// Go 1.21 及之前
for _, v := range []string{"a", "b", "c"} {
go func() {
fmt.Println(v) // 错误:闭包捕获了变量 v (引用捕获)
}()
}
// 输出往往是 c c c旧版本解决方案
在 Go 1.22 之前,必须使用局部变量遮蔽或参数传递来解决:
for _, v := range arr {
v := v // 关键:创建新变量 v,遮蔽外层的迭代变量 v
go func() {
fmt.Println(v) // 此时捕获的是循环内部的新变量
}()
}2. 新语义 (Go >= 1.22)
从 Go 1.22 (2024 年 2 月发布) 开始,for 循环的语义被修改了:每次迭代都会创建新的变量。
上述的“坑”在 Go 1.22+ 中自动修复,不再需要手动写 v := v。
官方参考资料
- Proposal (GitHub Issue): spec: less error-prone loop variable scoping #60078
- Go Blog: Fixing For Loops in Go 1.22
- Go Wiki: LoopvarExperiment
3. 常见误区:为什么我升级了 Go 还是旧行为?
这是面试和实战中非常容易踩的坑。即使你安装了最新的 Go 版本,代码行为可能仍然是旧的。
原因一:go.mod 版本限制 (最常见)
Go 编译器会为了向后兼容,严格遵循当前项目 go.mod 文件中声明的 go 版本。
如果你的 go.mod 文件里写的是 go 1.21 或更低,编译器就会强制使用旧的循环语义,无论你的工具链版本有多高。
解决方法:
修改 go.mod 中的版本号为 1.22 或更高。
go mod edit -go=1.22原因二:编译器优化 (变量未逃逸)
如果你在 Go 1.22+ 中使用 println(&v) (内置函数) 打印地址,可能会发现地址看起来是一样的。
这是编译器的优化。虽然语义上每次迭代都是新变量,但如果这个变量没有逃逸(没有被外部引用、没有存入堆中),编译器为了性能,会复用同一个栈内存空间。
一旦你让变量逃逸(例如存入切片、使用 fmt.Printf 打印),Go 1.22 就会强制分配新的内存地址。
// Go 1.22+
arr := []int{1, 2}
// 场景 1:变量未逃逸 -> 编译器优化复用栈空间 -> 地址可能相同
for _, v := range arr {
println(&v)
}
// 场景 2:变量逃逸 (fmt.Printf) -> 强制分配新地址 -> 地址一定不同
for _, v := range arr {
fmt.Printf("%p\n", &v)
}总结
| 特性 | Go < 1.22 | Go >= 1.22 |
|---|---|---|
| 迭代变量地址 | 固定不变 (复用) | 语义上每次变化 (新建) (但在未逃逸时可能被优化复用) |
| 取地址结果 | 所有指针指向同一地址 | 指向各自迭代的独立地址 |
| 闭包捕获行为 | 捕获同一变量 (通常是最后的值) | 捕获各自迭代的独立变量 |
| 推荐写法 | 需手动 v := v 或传参 | 直接使用即可 |