因为go的for循环复用同一变量i的内存地址,所有闭包共享该变量指针;当闭包执行时循环已结束,i值固定为终值(如len(slice)),属变量作用域与生命周期理解偏差。

闭包捕获循环变量时,为什么 i 总是最后一个值?
因为 Go 的 for 循环复用同一个变量内存地址,所有闭包共享这个 i 的指针。等 goroutine 真正执行时,循环早已结束,i 停在终值(比如 len(slice))。
这不是并发 bug,而是变量作用域和生命周期的理解偏差。即使不用 goroutine,纯闭包也会复现:
for i := 0; i < 3; i++ {
f := func() { fmt.Println(i) }
f() // 输出 1, 2, 3?不,全是 3
}
- 根本原因:Go 中
for的迭代变量i是单个绑定,每次迭代只是改写其值,不是新建变量 - 闭包捕获的是变量的地址,不是当前值的副本
- goroutine 延迟执行放大了这个问题,但本质与并发无关
用 := 在循环内声明新变量就能解决?
可以,但必须确保新变量在每次迭代中独立创建——最稳妥的方式是在循环体内用 := 声明一个**基于当前值的新局部变量**。
for i := 0; i < 3; i++ {
i := i // 关键:显式创建新变量,值拷贝
go func() {
fmt.Println(i) // 此处 i 是独立副本,输出 0 1 2
}()
}
- 这行
i := i不是冗余:它在每次迭代中分配新栈空间,闭包捕获的是这个新i的地址 - 不能写成
var i = i(语法错误),也不能省略声明直接用go func(i int)(见下一点) - 注意:这种写法对指针类型同样有效,比如
ptr := &slice[i],闭包捕获的是该次迭代的ptr地址,而非循环变量本身
go func(i int) 形参传值为什么安全?
因为函数参数是值传递,每次调用都会把当前 i 的值拷贝进新栈帧,闭包实际捕获的是这个形参变量的地址——而每个 goroutine 调用都拥有自己独立的形参栈空间。
立即学习“go语言免费学习笔记(深入)”;
for i := 0; i < 3; i++ {
go func(i int) { // i 是形参,每次调用都是新变量
fmt.Println(i) // 安全,输出 0 1 2
}(i) // 立即传入当前 i 的值
}
- 必须立即调用:
(i)不能省略,否则闭包没被触发,i还是外层循环变量 - 如果函数体较长或需复用,建议封装为独立函数,避免闭包嵌套过深
- 性能上无额外负担:int 拷贝成本极低;但若传大结构体,要考虑是否真需要值拷贝,还是应传指针 + 显式拷贝逻辑
当循环变量是指针,闭包又捕获了它,会发生什么?
危险:你可能以为捕获的是“当前元素地址”,但实际上捕获的是循环变量指针本身的地址——而该指针的值在循环中不断被覆盖。最终所有闭包看到的,可能是同一个被反复赋值的指针变量所指向的最后一个目标。
for _, v := range []*int{&a, &b, &c} {
go func() {
fmt.Println(*v) // 全部打印 *c 的值,因为 v 指针本身被复用
}()
}
- 正确做法不是传
&v(那是取循环变量指针的地址,更混乱),而是先解引用再存值:val := *v,或直接传值:go func(val int) { ... }(*v) - 如果必须保留指针语义(比如要修改原值),就得确保每个 goroutine 操作的是不同目标:用
v := v创建新指针变量,它保存的是当前迭代的原始指针值,不会随循环改变 - 切片、map、channel 等引用类型同理:闭包捕获的是变量本身(如
m),不是它指向的数据;若m在循环中被重新赋值,后续闭包看到的就是新值










