waitgroup 值传递导致 done() 无效:按值传参会复制互斥锁和计数器,子 goroutine 调用 done() 只影响副本,主 goroutine 的 wait() 永不返回;必须用指针传递并声明为 *sync.waitgroup。

WaitGroup 值传递导致 Done() 完全无效
这是最隐蔽也最容易复现的死锁:把 sync.WaitGroup 当普通结构体按值传入 goroutine,比如 go downloadFile(url, wg)。Go 会复制整个结构体,而 sync.WaitGroup 内部包含 sync.Mutex 和计数器字段,副本之间互斥锁和计数器完全独立。
子 goroutine 调用 wg.Done() 只是减少副本计数,main 中原始 wg.Wait() 永远等不到归零。
-
go vet会直接报错:passes sync.WaitGroup by value - 必须改为指针传递:
go downloadFile(url, &wg),且函数签名要声明为func downloadFile(url string, wg *sync.WaitGroup) - IDE(如 GoLand、VS Code + gopls)通常会在参数位置标黄并提示 “contains sync.Locker”
defer wg.Done() 放在函数中间或末尾
一旦前面有 return err 或 panic,defer wg.Done() 就根本不会执行。HTTP 请求失败、文件权限不足、IO 中断等常见错误路径都会跳过它,导致 wg.Wait() 卡死。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 正确做法:把
defer wg.Done()放在函数第一行 - 配合
if err != nil早期返回,能显著降低遗漏风险 - 不要依赖“逻辑上一定会走到最后”——并发代码的错误路径比想象中更常见
向无缓冲 channel 发送数据但没人接收
典型死锁链:goroutine 执行 out → 因 <code>out 是无缓冲通道且无人接收而阻塞 → defer wg.Done() 不执行 → wg.Wait() 永不返回。
- 修复核心:确保发送端永远有接收者 —— 必须启动一个独立
goroutine专门消费out - 不能把接收逻辑放在
wg.Wait()之后,那已经太晚;必须在启动 worker 前就启动 receiver - 用带缓冲通道(如
make(chan int, N))只是缓解,不是解决;缓冲大小必须 ≥ 并发数,否则仍有概率阻塞
Add() 与 Done() 数量不匹配
sync.WaitGroup 计数器不是“任务完成数”,而是“尚未完成的 goroutine 数”。漏调一次 Done() 或多调一次 Add() 都会导致 Wait() 永久挂起。
- 常见于循环外一次性
wg.Add(len(urls)),但实际只启动了 1 个 goroutine - 或在
for循环里漏掉wg.Add(1) - 闭包捕获循环变量导致多个 goroutine 共享同一个
Done()调用(例如for i := range urls { go func() { defer wg.Done() }() })
真正难调试的不是单点错误,而是多个问题叠加:比如值传递 + defer 放错位置 + 无缓冲 channel,这种组合会让 go run 直接 panic fatal error: all goroutines are asleep - deadlock!,堆栈却只显示 Wait() 调用点,看不出哪条路径没走完。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










