这个问题在 Go 1.22 版本前后有完全不同的答案。

核心结论

  1. Go 1.22 之前不会变化for range 的迭代变量是复用的,整个循环过程中,迭代变量的内存地址始终固定。
  2. 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

官方参考资料


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.22Go >= 1.22
迭代变量地址固定不变 (复用)语义上每次变化 (新建)
(但在未逃逸时可能被优化复用)
取地址结果所有指针指向同一地址指向各自迭代的独立地址
闭包捕获行为捕获同一变量 (通常是最后的值)捕获各自迭代的独立变量
推荐写法需手动 v := v 或传参直接使用即可