Go中for循环启动goroutine时变量被复用:循环变量i在整个循环中指向同一内存地址,协程异步执行时i已为终值,导致所有goroutine读取到相同值。

for循环中启动goroutine时变量被复用
Go里最典型的并发陷阱:在for循环里直接用循环变量启动goroutine,结果所有协程看到的都是最后一次迭代的值。这不是Go的bug,而是变量作用域和闭包捕获机制共同导致的——for循环体内的变量(如i、v)在整个循环生命周期中是**同一个内存地址**,goroutine异步执行时,循环早已结束,i早已变成终值。
常见错误写法:
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // 全部输出 3
}()
}
解决方法只有两种可靠路径:
- 在循环体内用局部变量显式拷贝:
go func(i int) { fmt.Println(i) }(i) - 用
let-style声明(Go 1.22+支持range中:=绑定新变量),但更通用的是在循环内加val := v再传入闭包 - 避免在闭包中直接引用外部循环变量,尤其不要依赖
range返回的v(它是复用的)
range遍历切片/映射时v是值拷贝但地址可能被误用
range对切片或映射遍历时,v确实是每次迭代的副本,但如果你对v取地址(&v),拿到的永远是**同一个栈地址**,因为v被复用。这在启动goroutine或存入指针切片时会出问题。
立即学习“go语言免费学习笔记(深入)”;
错误示例:
data := []string{"a", "b", "c"}
var ptrs []*string
for _, v := range data {
ptrs = append(ptrs, &v) // 全是指向同一个v的地址
}
fmt.Println(*ptrs[0], *ptrs[1], *ptrs[2]) // 全是"c"
修复方式很简单:
- 用索引访问原切片:
&data[i] - 在循环内声明新变量并取其地址:
val := v; ptrs = append(ptrs, &val) - 如果目标是存结构体指针,直接构造新结构体再取地址,不复用
v
goroutine中调用defer或闭包捕获变量时机易混淆
defer语句在函数定义时就确定了参数求值时机(不是执行时),而goroutine中的闭包捕获变量也遵循同样规则:捕获的是变量的**当前值**还是**未来值**,取决于你是否在闭包定义前完成求值。
比如这段代码:
for i := 0; i < 3; i++ {
defer func() {
fmt.Print(i) // 输出 3 3 3,因为defer注册时i还没变,但执行时i=3
}()
}
原因:defer注册的是函数值,但i是外部变量,真正执行时才读取——和goroutine一样。要修正,必须立即求值:
- 传参:
defer func(i int) { fmt.Print(i) }(i) - 或者在defer前赋值:
ii := i; defer func() { fmt.Print(ii) }() - 注意:defer在goroutine中使用更要小心,它不会“随goroutine生命周期延迟”,而是在所在函数return时触发
sync.WaitGroup配合for循环漏Add或Add位置错
WaitGroup的Add必须在goroutine启动前调用,且不能放在goroutine内部——否则存在竞态:主goroutine可能先执行Wait(),而子goroutine还没来得及Add,导致panic或提前返回。
典型错误:
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
go func() {
defer wg.Done()
wg.Add(1) // 错!Add在Done之后,且位置不对
fmt.Println(i)
}()
}
wg.Wait() // 可能panic: negative WaitGroup counter
正确顺序唯一:
-
wg.Add(1)必须在go语句之前,或至少在goroutine开始执行逻辑的第一行 - 推荐写法:
wg.Add(1); go func() { defer wg.Done(); ... }() - 如果循环体复杂,把
Add提到循环开头比塞进goroutine里更安全
变量复用问题从来不是语法缺陷,而是开发者对“变量生命周期”和“协程调度时机”的预期偏差。最稳妥的做法,永远是让每个goroutine拥有自己独占的一份数据副本,而不是共享一个随时会被覆盖的栈变量。










