本文详解 Go 并发编程中因循环变量被多个 goroutine 共享而导致的意外输出问题,重点剖析 for 循环中匿名函数捕获变量 j 的机制,并提供两种简洁可靠的修复方式。
本文详解 go 并发编程中因循环变量被多个 goroutine 共享而导致的意外输出问题,重点剖析 `for` 循环中匿名函数捕获变量 `j` 的机制,并提供两种简洁可靠的修复方式。
在 Go 中使用 goroutine 配合循环启动多个并发任务时,一个高频且隐蔽的 Bug 是:所有 goroutine 最终都打印或使用了循环变量的最终值(如 40),而非各自预期的迭代值(0, 1, 2, ..., 39)。你提供的代码正是典型示例:
for j := 0; j < MAX_TH; j++ {
go func() { getWebPageContent("http://www.example.com", messages, j) }()
}问题根源在于:Go 的 for 循环复用同一个变量 j 的内存地址。每次迭代只是更新 j 的值,而所有匿名函数共享这个单一变量实例。当 goroutine 实际执行时(通常远晚于循环结束),j 已经递增至 40(循环终止条件),因此绝大多数 goroutine 读取到的 j 都是 40 —— 这就是你观察到“几乎总是输出 40”的根本原因。
✅ 正确做法:为每次迭代创建独立的变量副本。Go 提供两种惯用且清晰的解决方式:
方式一:在循环体内显式声明新变量(推荐)
for j := 0; j < MAX_TH; j++ {
j := j // ✅ 创建同名但作用域独立的新变量
go func() {
getWebPageContent("http://www.example.com", messages, j)
}()
}这行 j := j 并非冗余赋值,而是利用 Go 的短变量声明语法,在每次迭代中创建一个新的局部变量 j,其值绑定当前迭代的数值。每个 goroutine 捕获的是自己专属的 j,互不干扰。
方式二:将变量作为参数传入闭包
for j := 0; j < MAX_TH; j++ {
go func(val int) {
getWebPageContent("http://www.example.com", messages, val)
}(j) // ✅ 立即传入当前 j 值
}通过函数参数传递,确保闭包内部使用的 val 是调用时刻的快照值。
⚠️ 注意事项:
- go vet 工具能静态检测此类问题,建议开发中始终启用:go vet ./...
- ioutil.ReadAll 已被弃用(Go 1.16+),生产环境请改用 io.ReadAll;
- http.Get 后需检查 r != nil 再调用 r.Body.Close(),避免 panic;
- messages channel 无需显式 close()(当前逻辑无写端阻塞风险),但若后续扩展为带缓冲或有其他写者,需谨慎管理关闭时机。
修复后,程序将稳定输出 0 到 39 的乱序整数(符合 goroutine 调度不确定性),验证并发逻辑正确性。记住:在循环中启动 goroutine 时,永远不要直接捕获循环变量——显式复制才是安全之道。


















