sync.WaitGroup 本身线程安全,死锁主因是值传递、defer wg.Done() 位置错误或无缓冲通道阻塞;正确做法是传指针、defer 放函数首行、前置启动通道消费者。

Go 中 sync.WaitGroup 本身是线程安全的,但“并发修改导致死锁”这个说法不准确——真正的问题从来不是并发修改,而是**值传递、调用顺序错乱或通道阻塞导致 Done() 永远不被执行**。只要用法合规,sync.WaitGroup 的 Add()、Done()、Wait() 方法内部已加锁,完全支持多 goroutine 安全调用。
sync.WaitGroup 值传递触发死锁的典型现象
程序启动后卡住,最终报错:fatal error: all goroutines are asleep - deadlock!。这不是偶发,而是必然发生:所有 worker goroutine 已退出,主线程却仍在 wg.Wait() 上挂起。
- 常见错误写法:
go worker(url, wg),且worker函数签名是func worker(url string, wg sync.WaitGroup) -
go vet会直接警告:passes sync.WaitGroup by value - IDE(如 VS Code + gopls)会在参数位置标黄,提示 “contains sync.Locker”
- 根本原因:值传递复制了整个结构体,包括内部
sync.Mutex字段;子 goroutine 调用的defer wg.Done()只影响副本,主线程等待的原始wg计数器始终不变
defer wg.Done() 放错位置导致漏调
函数中途返回,defer 根本没机会执行,Done() 就永远丢失——这是比值传递更隐蔽的坑,尤其在易出错路径上。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 错误示例:把
defer wg.Done()写在 HTTP 请求之后、错误检查之前 - 一旦
http.Get()返回非 nil error,函数立即 return,defer不触发 - 正确做法:把
defer wg.Done()放在函数第一行,紧随参数声明之后 - 配合早期返回(
if err != nil { return err })才能确保所有退出路径都被覆盖
与无缓冲通道混用时的隐性死锁链
不是 WaitGroup 本身出问题,而是它和通道协作时的控制流设计失误,形成“谁等谁”的循环依赖。
- 典型场景:worker goroutine 向无缓冲通道
out发送结果 → 因无人接收而永久阻塞 →defer wg.Done()永不执行 →wg.Wait()永不返回 - 错误顺序:
for range urls { go worker(...) }→wg.Wait()→for range out { ... } - 正确顺序:先启动一个 goroutine 消费
out(哪怕只是丢弃),再启动 worker,最后wg.Wait() - 缓冲通道不是解药:设
make(chan int, N)只是延迟死锁,不能解决“发送端是否总能被消费”这个本质问题
最麻烦的往往不是单点错误,而是多个陷阱叠加:比如值传递 + defer 放错位置 + 无缓冲通道 + 循环变量闭包捕获。每个单独看都容易修复,合在一起就极难定位。实际调试时,优先检查 go vet 输出,再确认 defer wg.Done() 是否真正在第一行,最后审视通道消费逻辑是否前置启动。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










