
本文深入解析 Go 中 sync.WaitGroup 未按预期阻塞的典型问题,核心在于 goroutine 中对循环变量的错误捕获——所有协程共享同一变量实例,导致数据竞争与逻辑错乱。
本文深入解析 go 中 `sync.waitgroup` 未按预期阻塞的典型问题,核心在于 goroutine 中对循环变量的错误捕获——所有协程共享同一变量实例,导致数据竞争与逻辑错乱。
在使用 sync.WaitGroup 控制并发任务生命周期时,一个高频且隐蔽的错误是:wg.Wait() 过早返回,协程尚未执行完毕,甚至输出全为零或 panic。问题往往并非 WaitGroup 本身失效,而是闭包捕获了循环变量的地址而非值。
以原始代码为例:
func printSize(listOfUrls []string) {
var wg sync.WaitGroup
wg.Add(len(listOfUrls))
for _, myurl := range listOfUrls {
go func() {
body := getUrlBody(myurl) // ⚠️ 危险:myurl 是循环变量的引用!
fmt.Println(len(body))
wg.Done()
}()
}
wg.Wait() // 可能立即返回——因为所有 goroutine 都在用同一个(已迭代完毕的)myurl
}由于 for 循环中的 myurl 是单个变量,每次迭代仅更新其值,而匿名函数 func(){...} 捕获的是该变量的内存地址。当循环迅速结束、goroutines 尚未调度执行时,myurl 已指向最后一个元素(甚至超出范围),导致所有 goroutine 实际请求的是错误 URL 或空字符串,getUrlBody("") 返回空切片,len(body) 为 0。
✅ 正确解法一:将变量作为参数传入闭包(推荐)
通过函数参数显式传递当前迭代值,确保每个 goroutine 拥有独立副本:
func printSize(listOfUrls []string) {
var wg sync.WaitGroup
wg.Add(len(listOfUrls))
for _, myurl := range listOfUrls {
go func(url string) { // ← 参数 url 是独立拷贝
body := getUrlBody(url)
fmt.Println("URL:", url, "Length:", len(body))
wg.Done()
}(myurl) // ← 立即传入当前 myurl 值
}
wg.Wait()
}✅ 正确解法二:在循环体内创建同名新变量(语义清晰)
利用 Go 的短变量声明 := 在每次迭代中创建新的局部变量,切断与原循环变量的绑定:
func printSize(listOfUrls []string) {
var wg sync.WaitGroup
wg.Add(len(listOfUrls))
for _, myurl := range listOfUrls {
myurl := myurl // ← 创建新变量,遮蔽外层 myurl
go func() {
body := getUrlBody(myurl) // ← 此时 myurl 是独立值
fmt.Println("URL:", myurl, "Length:", len(body))
wg.Done()
}()
}
wg.Wait()
}⚠️ 注意事项与最佳实践
- 永远不要在 goroutine 闭包中直接使用循环变量(如 for i := range xs { go func(){ use(i) }() }),这是 Go 并发编程的“经典坑”。
- sync.WaitGroup 必须在 Add() 后、Go 启动前调用,且 Done() 调用次数必须严格等于 Add() 参数值,否则会 panic 或死锁。
- 若需错误处理或结果收集,建议配合 errgroup.Group(来自 golang.org/x/sync/errgroup)替代裸 WaitGroup。
- 使用 go vet 工具可检测部分闭包变量捕获问题(虽非 100% 覆盖,但值得启用)。
掌握这一闭包行为本质,不仅能修复 WaitGroup 不等待的问题,更是写出健壮 Go 并发代码的关键基础。










