WaitGroup死锁是程序卡住不报错,主goroutine在wg.Wait()永久阻塞,因值传递导致子goroutine调用的是wg副本的Done(),主wg计数器始终不归零;Add必须在go前调用,Done必须defer在goroutine内执行。

WaitGroup 死锁不是 panic,而是程序卡住不动、不报错、也不退出——最典型表现就是 fatal error: all goroutines are asleep - deadlock!。根本原因几乎总是:主线程在 wg.Wait() 处等待,但所有子 goroutine 已退出,却没人真正调用过原始 wg.Done()。
为什么传值会导致 WaitGroup 死锁
sync.WaitGroup 是一个含 sync.Mutex 和计数器的结构体,Go 禁止对其做值拷贝(go vet 会直接报 passes sync.WaitGroup by value)。一旦你写成:
func worker(id int, wg sync.WaitGroup) { defer wg.Done() }每个 goroutine 收到的都是独立副本,wg.Done() 修改的是副本计数器,主线程的 wg 计数器始终不变。
- 值传递时,
wg.Add(1)在主线程调用,但wg.Done()在副本上调用 → 计数器永远不归零 -
go vet能立刻发现该问题,务必加入 CI 或本地 pre-commit 钩子 - 即使结构体没显式包含
sync.Mutex,只要内部有不可复制字段,值传参就危险
WaitGroup 使用顺序错误引发的隐性卡死
计数器操作必须严格遵循「Add 在启动前、Done 在结束时、Wait 在最后」。常见反模式:
立即学习“go语言免费学习笔记(深入)”;
go func() { wg.Add(1); defer wg.Done(); ... }()此时 wg.Add(1) 在 goroutine 内部执行,主线程极可能先跑完 wg.Wait(),导致提前返回、任务漏执行。
-
wg.Add(n)必须在go语句之前,且 n 必须等于实际启动的 goroutine 数量 - 循环中启动多个 goroutine 时,别用闭包捕获循环变量(如
for i := range list { go f(i) }),否则所有 goroutine 可能拿到同一个i值 - 如果某条路径可能跳过
defer wg.Done()(比如函数中途 return),就会永久卡住wg.Wait()
WaitGroup 和 channel 混用时的双重阻塞
当 WaitGroup 和无缓冲 channel 一起用,容易形成“谁等谁”的循环依赖。例如:
go func() { wg.Add(1); defer wg.Done(); ch <- result }() // ch 是无缓冲 channel若主 goroutine 还没开始 range ch 或 <-ch,这个 goroutine 就会卡在 ch <- result,永远没机会执行 defer wg.Done(),导致 wg.Wait() 永不返回。
- 无缓冲 channel 的发送操作是同步的,必须有接收方同时就位
- 不要把
wg.Wait()放在 channel 接收循环里(比如for { select { case ) - 更安全的做法:用带缓冲 channel,或确保接收 goroutine 先启动;关闭 channel 的时机必须在所有发送完成之后(即
wg.Wait()返回后)
WaitGroup 本身不难,难的是它和 goroutine 启动时序、channel 同步、错误分支覆盖交织在一起。最易被忽略的是:Done 是否真被调用了——不是“写了 defer”,而是“执行到了”。加日志、用 go vet、在关键路径补 panic("done not called") 断言,比靠猜快得多。


















