在 go 的 goroutine 中直接使用 for 循环变量会导致所有协程共享同一个变量地址,造成输出结果异常(如全部打印 i=100),根本原因是闭包捕获的是变量引用而非值;正确做法是在每次迭代中创建局部副本。
在 go 的 goroutine 中直接使用 for 循环变量会导致所有协程共享同一个变量地址,造成输出结果异常(如全部打印 i=100),根本原因是闭包捕获的是变量引用而非值;正确做法是在每次迭代中创建局部副本。
在 Go 并发编程中,一个经典且极易踩坑的问题是:
在 for 循环中启动多个 goroutine,并在匿名函数内直接引用循环变量(如 i)
。初学者常误以为每个 goroutine 会“快照”当前的 i 值,但实际行为截然不同。
问题核心在于:Go 的匿名函数形成闭包时,
捕获的是变量的内存地址(即引用),而非其当前值
。由于 for 循环本身运行极快,而 goroutine 的调度存在延迟,当这些 goroutine 真正开始执行时,外层循环早已结束,此时循环变量 i 已稳定为终值(100)。因此,所有 goroutine 打印的 i 都是同一个最终值——导致输出类似:
更危险的是,若未显式同步(如 fmt.Scanln() 阻塞主 goroutine),程序可能在部分 goroutine 执行前就退出,造成不可预测的竞态行为。
✅ 正确解法:
在每次迭代中创建独立的局部变量副本
,确保每个 goroutine 捕获的是专属值:
或等价地使用带参数的匿名函数(如原答案所示):
⚠️ 注意事项:
不要依赖 runtime.GOMAXPROCS 来“修复”此问题——它仅控制并行度,不改变变量捕获机制;
Go 1.22+ 支持在 for range 循环中直接对迭代变量做 := 赋值实现自动副本(但传统 for init; cond; post 仍需手动处理);
若需传递多个变量,统一用局部副本或结构体封装,避免混合引用与值语义;
使用 go vet 或静态分析工具(如 staticcheck)可检测此类常见闭包陷阱。
总结:Go 的闭包按引用捕获变量是设计使然,不是 bug。理解这一机制,并养成在循环中显式创建副本的习惯,是编写健壮并发代码的关键基础。
0 100
1 100
2 100
...
99 100for i := 0; i < 100; i++ {
i := i // ✅ 关键:声明同名局部变量,遮蔽外层 i(shadowing)
go func() {
fmt.Println(i) // 此处 i 是当前迭代独有的副本
}()
}for i := 0; i < 100; i++ {
go func(n int) {
fmt.Println(n) // n 是传入的值拷贝,安全
}(i)
}