waitgroup卡死主因是值传递、defer位置错误和add调用时机不当:必须传&wg指针,defer wg.done()置于函数首行,wg.add(1)须在go语句前调用。

WaitGroup 卡在 wg.Wait() 不返回?先查值传递还是指针传递
绝大多数卡死问题,根源就在这里:你把 sync.WaitGroup 当作普通参数传进 goroutine,却没加 &。Go 对结构体默认按值复制,wg.Done() 在子 goroutine 里操作的只是副本,main 中的原始 wg 计数器纹丝不动,wg.Wait() 只能一直等。
检查你的 goroutine 启动代码:
- ❌ 错误写法:
go downloadFromURL(url, wg)—— 传的是副本 - ✅ 正确写法:
go downloadFromURL(url, &wg)—— 必须传地址
同时函数签名必须同步改成接收 *sync.WaitGroup,否则编译都过不去。
defer wg.Done() 放错位置,等于没写
很多人把 defer wg.Done() 往函数末尾一丢,觉得“反正最后执行就行”。但一旦前面有 return err,这行 defer 就根本不会注册,Done() 永远不调用。
正确姿势只有一条:把它放在函数第一行(或至少在任何可能提前 return 的逻辑之前):
func downloadFromURL(url string, wg *sync.WaitGroup) error {
defer wg.Done() // ✅ 这里!确保所有退出路径都触发
content, err := os.Create("temp_docs/" + fileName)
if err != nil {
return err // defer 仍会执行
}
defer content.Close()
// ...
}
别信“我这个函数逻辑简单,不会提前 return”——HTTP 超时、磁盘满、目录不存在……错误比你想象中来得勤快得多。
wg.Add() 在 goroutine 里调用?计数器直接乱套
如果你在 goroutine 内部调用 wg.Add(1),而不是在启动前调用,就会出现两种危险情况:
- 主线程跑太快,
wg.Wait()在所有Add()执行前就进入等待 → 等待零个任务,立刻返回,goroutine 被丢弃 - 更糟的是,多个 goroutine 并发
Add(),而Add()不是原子操作 → 可能 panic:negative WaitGroup counter
务必遵守铁律:wg.Add(n) 必须在 go 语句之前、且在同一个 goroutine 中调用:
for _, url := range urls {
wg.Add(1) // ✅ 这里!不是在 downloadFromURL 里
go downloadFromURL(url, &wg)
}
通道阻塞也会让 WaitGroup 假死?别忽略接收端
当 WaitGroup 和 channel 混用时,一个隐蔽杀手是:工作 goroutine 执行完逻辑后,试图往无缓冲 channel(比如 out )发数据,但主 goroutine 还没开始读 —— 它正卡在 <code>wg.Wait() 上等你结束。结果所有 goroutine 全部卡在发送处,defer wg.Done() 根本没机会执行。
这不是 WaitGroup 的 bug,而是调度顺序错了。解法很明确:
- 要么给 channel 加缓冲(如
make(chan Item, 10)),缓解阻塞压力 - 要么显式起一个接收 goroutine,在
wg.Wait()之前就开干:go func() { for range out { /* consume */ } }()
记住:WaitGroup 只管“协程是否退出”,不管“协程卡在哪”。它不感知通道阻塞,但你会被它拖住。
真正难排查的,往往是多个错误叠加:比如值传递 + defer 放错 + 通道阻塞。单看任一环节都像对的,合起来就是死锁。动手前,先盯住三件事:&wg 传了没、defer wg.Done() 是不是第一行、wg.Add() 是不是在 go 外面 —— 这三点对了,八成问题就没了。











