
本文详解 Go 并发编程中因循环变量被闭包共享导致的典型竞态问题,揭示为何 goroutine 中输出总是 40,并提供两种标准、安全的修复方案(显式传参或变量重声明),确保每个 goroutine 持有独立的循环值。
本文详解 go 并发编程中因循环变量被闭包共享导致的典型竞态问题,揭示为何 goroutine 中输出总是 `40`,并提供两种标准、安全的修复方案(显式传参或变量重声明),确保每个 goroutine 持有独立的循环值。
在 Go 中使用 for 循环启动多个 goroutine 时,若直接在匿名函数中引用循环变量(如 j),极易遭遇变量捕获陷阱:所有 goroutine 实际共享同一个变量实例,而非各自独立的副本。这正是你代码中始终输出 40(即 MAX_TH)的根本原因。
问题根源分析
你的原始循环:
for j := 0; j < MAX_TH; j++ {
go func() { getWebPageContent("http://www.example.com", messages, j) }()
}在循环结束前,j 已自增至 40 并退出循环。而所有 goroutine 启动后延迟执行 getWebPageContent(..., j) 时,读取的已是最终值 40——因为它们捕获的是变量 j 的内存地址,而非其某次迭代的快照。
此行为符合 Go 规范,但常被误认为“bug”。go vet 工具会明确警告:loop variable j captured by func literal。
正确解决方案
✅ 方案一:循环内重声明变量(推荐,简洁清晰)
在每次迭代中创建 j 的新绑定:
for j := 0; j < MAX_TH; j++ {
j := j // 关键:为本次迭代创建独立的 j
go func() {
getWebPageContent("http://www.example.com", messages, j)
}()
}j := j 利用 Go 的短变量声明语法,在当前作用域内定义一个与外层同名但独立的新变量,确保每个 goroutine 引用的是专属副本。
✅ 方案二:通过函数参数传递值
将循环变量作为参数传入闭包:
for j := 0; j < MAX_TH; j++ {
go func(val int) {
getWebPageContent("http://www.example.com", messages, val)
}(j) // 立即传入当前 j 值
}此时 val 是闭包的局部参数,天然隔离,语义更显式。
完整修复后的代码示例
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func getWebPageContent(url string, c chan int, val int) interface{} {
if r, err := http.Get(url); err == nil {
defer r.Body.Close()
if body, err := ioutil.ReadAll(r.Body); err == nil {
c <- val // 发送本次 goroutine 对应的序号
return string(body)
}
} else {
fmt.Printf("HTTP error for %d: %v\n", val, err)
}
return "XoX"
}
const MAX_TH = 40
func main() {
messages := make(chan int, MAX_TH) // 设置缓冲区避免阻塞
for j := 0; j < MAX_TH; j++ {
j := j // 修复:创建独立变量
go func() {
getWebPageContent("http://www.example.com", messages, j)
}()
}
// 接收全部结果
for i := 0; i < MAX_TH; i++ {
page_number := <-messages
fmt.Println("Page number: ", page_number)
}
close(messages) // 可选:关闭通道(接收端已知数量,非必须)
}注意事项与最佳实践
- 始终启用 go vet:它能自动检测此类闭包捕获问题,是开发阶段的必备检查。
- 避免在 goroutine 中直接使用循环变量:无论 for i := range slice 还是 for i := 0; i
- 合理设置 channel 缓冲:本例中 make(chan int, MAX_TH) 可防止发送端因接收未就绪而阻塞(尤其在高并发场景)。
- 错误处理不可忽略:真实项目中需对 HTTP 请求失败、超时等做健壮处理,避免 goroutine 泄漏。
- 考虑使用 sync.WaitGroup 替代计数器:更符合 Go 并发惯用法,提升可读性与安全性。
遵循以上原则,即可彻底规避循环变量闭包陷阱,写出确定、可靠、易维护的 Go 并发代码。


















