waitgroup 值传递、add() 时机错误、defer wg.done() 位置不当是三大致命错误:值传参导致 done() 失效;add() 必须在 go 前调用,否则 panic 或漏等待;defer wg.done() 必须置于函数首行,否则可能不执行。

sync.WaitGroup 用错一次,程序就卡死或 panic,不是性能问题,而是逻辑崩溃。 它不报错、不提示、不超时,只安静地 hang 住,或者在某个随机时刻 panic: sync: negative WaitGroup counter —— 这三个错误,90% 的 Go 并发代码都踩过。
WaitGroup 值传递导致 Done() 完全失效
这是最隐蔽也最致命的错误:你传的是 wg,但子函数操作的是它的副本。原始 wg 的计数器从没被减过,wg.Wait() 永远不会返回。
- 错误写法:
go downloadFromURL(url, wg)(wg是值传参) - 正确写法:
go downloadFromURL(url, &wg),且函数签名必须是func downloadFromURL(url string, wg *sync.WaitGroup) error - 切片或 map 里存
sync.WaitGroup?不行。只能存*sync.WaitGroup - 连赋值都要小心:
w2 := wg是复制,w2和wg已经无关
Add() 调用时机错位引发 panic 或漏等待
wg.Add(1) 必须在 go 语句之前调用;否则要么计数器为负 panic,要么主 goroutine 提前 Wait() 返回,子 goroutine 被丢弃。
- 错误写法:
go func() { wg.Add(1); ... }()或循环里把wg.Add(1)放在go后面 - 正确顺序:先
wg.Add(1),再go f(),哪怕只差一行,时序就可能崩 - 循环中别写成:
for _, t := range tasks { go f(&wg, t); wg.Add(1) }—— 这是错的,go和Add顺序反了 - 如果
Add(-1)或先Done()后Add(),运行时直接 panic
defer wg.Done() 放错位置,等于没写
defer 不是“保证执行”,而是“函数返回前执行”。如果 defer wg.Done() 写在 return 之后、或被提前 return 跳过,它根本不会注册,更不会执行。
- 错误写法:函数末尾才写
defer wg.Done(),或写在if err != nil { return err }后面 - 正确写法:必须是函数第一行之一,
defer wg.Done()紧跟函数签名后立即出现 - 哪怕中间有 10 个
return分支,只要 defer 在开头,就一定触发 - 注意:panic 也会触发 defer,但前提是它已被注册 —— 所以位置比异常类型更重要
这三个错误都不涉及复杂逻辑,全是 Go 语言机制的底层细节:结构体复制语义、goroutine 启动异步性、defer 注册时机。它们不会在编译时报错,也不会在单元测试里暴露,往往只在压测或上线后某次缓冲区变化、某次网络延迟时突然爆发。真正难的不是写对,而是在每次写 go 前,下意识检查这三件事是否已闭环。











