所有goroutine都读取到循环结束后的最终值;因for复用同一变量地址,匿名函数捕获的是地址而非值,协程延迟执行时i已变为终值。

for循环里直接用循环变量启动goroutine会出什么问题
Go中在for循环内用go func() { ... }()启动协程时,如果直接引用循环变量(比如i或v),几乎所有情况下都会得到意外结果——所有协程看到的都是循环结束后的最终值。这不是Go的bug,而是变量作用域和闭包捕获机制共同导致的典型陷阱。
为什么循环变量在goroutine里总是“变”了
Go的for循环复用同一个变量内存地址,每次迭代只是更新它的值,而不是新建变量。而匿名函数捕获的是变量的**地址**(即引用),不是当前值的副本。当协程真正执行时,循环早已结束,i已变成终值(如len(slice)),所以所有协程读到的都是这个终值。
常见错误写法:
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // 全部输出 3
}()
}
正确做法是让每个goroutine拿到独立的值副本,方式有二:
- 把循环变量作为参数传入匿名函数:
go func(i int) { fmt.Println(i) }(i) - 在循环体内显式声明新变量:
for i := 0; i
range遍历切片/Map时v的问题更隐蔽
用range遍历时,v同样被复用。即使你只写go func() { fmt.Println(v) }(),所有goroutine最终打印的都是最后一次迭代的v值,尤其在处理结构体或指针时容易引发数据竞争或空指针 panic。
示例(危险):
data := []string{"a", "b", "c"}
for _, v := range data {
go func() {
fmt.Println(v) // 全部输出 "c"
}()
}
修复方式同上,但推荐第一种:传参。它语义清晰、无歧义:
for _, v := range data {
go func(val string) {
fmt.Println(val) // 输出 "a", "b", "c"
}(v)
}
sync.WaitGroup配合时还要注意别漏掉Add/Done
光修变量作用域还不够。如果用sync.WaitGroup等同步原语控制goroutine生命周期,漏掉wg.Add(1)或忘记在goroutine里调用wg.Done(),会导致主goroutine提前退出或死锁。
完整安全写法示例:
var wg sync.WaitGroup
data := []int{1, 2, 3}
for _, v := range data {
wg.Add(1)
go func(val int) {
defer wg.Done()
fmt.Println(val)
}(v)
}
wg.Wait()
最容易被忽略的是:变量捕获问题在编译期完全不报错,运行结果却随机或稳定出错;加上并发调度不可控,问题可能只在高负载或特定环境下暴露。











