sync.waitgroup仅在计数归零时唤醒等待者;add必须在goroutine启动前由主协程串行调用,done次数须严格匹配add总和,不可复制、不可重用,否则引发panic或死锁。

sync.WaitGroup 不是“等协程结束的万能胶”,它只做一件事:**计数归零后唤醒等待者**。用错就 panic 或死锁,没商量。
WaitGroup.Add 必须在 goroutine 启动前调用
这是最常触发 sync: WaitGroup misuse: Add called concurrently with Wait 的原因。
现象:程序偶尔 panic,或 Wait() 立即返回,后续 Done() 导致负计数 panic。
-
Add()和Wait()是竞态敏感操作,必须由主协程串行控制 - 绝不能写成:
go func() { wg.Add(1); defer wg.Done(); ... }() - 正确顺序只能是:
wg.Add(1); go func() { defer wg.Done(); ... }() - 循环启动多个协程时,
Add(1)必须在go语句之前,且一一对应
Done 调用次数必须严格匹配 Add 总和
Done() 等价于 Add(-1),底层对计数器做原子减法。一旦负值,立刻 panic。
- 分支逻辑里漏掉
defer wg.Done()(比如 error early return 后忘了)→ 计数卡住,Wait()永久阻塞 - 重复调用
Done()(例如 defer 放错位置、或手动调 + defer 双重调用)→ 计数变负 → panic - 闭包捕获循环变量时,多个协程可能共享同一变量,导致某个
Done()被跳过或误调 —— 用go f(x)显式传参规避
WaitGroup 不可复制,必须传指针
结构体字段赋值、函数参数传值、append 到切片、甚至作为 map value 存储,都会触发浅拷贝 —— 这会复制内部的 state1 字段,导致计数器分裂。
- 错误示例:
func handle(wg sync.WaitGroup)或subWg := wg - 正确做法:所有跨作用域使用都传
*sync.WaitGroup - 尤其注意:在测试中用
t.Run并发子测试时,别把wg闭包进子测试函数而未加锁或传指针
WaitGroup 不可重用,计数归零后不能再次 Wait
官方文档明确:计数器归零后,WaitGroup 进入终态。再次调用 Add() 是允许的(需确保无并发),但 Wait() 在计数非零时才能安全调用;若有人在归零后又 Add(n) 再 Wait(),虽不 panic,但行为依赖于内部 waiter 清理时机,属未定义行为。
- 不要复用同一个
wg变量处理多轮任务 —— 重新声明更清晰、更安全 - 没有 “reset” 方法,强行复用等于埋雷,尤其在长生命周期对象(如 HTTP handler)中
- 如果真要循环等待,每次都要新声明:
var wg sync.WaitGroup
Add/Done 的配对关系一眼可验。计数失配不会报编译错误,它只在运行时悄悄让程序挂住或崩掉。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











