sync.WaitGroup计数器变负数会立刻panic,错误信息为“panic: sync: negative WaitGroup counter”;必须先wg.Add()再启动goroutine,defer wg.Done()须置于函数开头,且WaitGroup必须指针传递、不可复用。

直接说结论:sync.WaitGroup 计数器变负数会立刻 panic,错误信息是 panic: sync: negative WaitGroup counter。这不是运行时偶发问题,而是调用顺序写错的确定性崩溃。
wg.Add() 必须在 go 语句之前执行
常见错误是把 wg.Add(1) 放到 goroutine 内部第一行,或者在 go func() { ... }() 之后才调用 Add。
- 现象:程序启动几毫秒就 panic,堆栈指向
Done()或Add() - 原因:goroutine 启动后可能瞬间执行
defer wg.Done(),而此时wg.Add(1)还没跑,计数器从 0 变 -1 - 正确顺序只能是:先
wg.Add(1),再go func() { defer wg.Done() ... }() - 批量启动时,推荐一次性
wg.Add(len(jobs)),比循环里每次Add(1)更安全
defer wg.Done() 必须放在 goroutine 函数开头
不是“快结束时 defer”,而是“一进函数就注册”。任何提前 return(比如 error 分支、条件判断、panic)都会跳过结尾逻辑,但 defer 仍会执行 —— 前提是你把它放对了位置。
- 错误写法:
if err != nil { return }写在defer wg.Done()前面 →Done()永远不注册 - 正确写法:第一行就是
defer wg.Done(),后面再处理业务逻辑 - 绝对禁止:
defer wg.Done()+ 手动wg.Done()→ 多减一次 → 负数 panic - 闭包传参时注意变量捕获:如果用
go func() { ... }(i),确保i是拷贝值,不是引用外部循环变量
*sync.WaitGroup 是必须的指针传递
值传递 sync.WaitGroup 看似能编译通过,但会导致 Done() 作用于副本,主线程永远等不到计数器归零。
立即学习“go语言免费学习笔记(深入)”;
- 错误:函数签名是
func worker(wg sync.WaitGroup),调用go worker(wg) - 正确:签名必须是
func worker(wg *sync.WaitGroup),调用go worker(&wg) - 底层原因:
sync.WaitGroup包含mutex字段,值传递会复制锁状态,Done()修改的是副本里的计数器 - 临时结构体字段或不可取址变量(如
someStruct{}.wg)也不能传地址,会编译报错
WaitGroup 不可复用,别试图 Reset
sync.WaitGroup 没有 Reset() 方法,Wait() 返回后也不能直接再 Add() 启动新任务 —— Go 1.21+ 已明确禁止这种用法。
- 错误现象:
panic: sync: WaitGroup is reused before previous Wait has returned - 根本风险:旧 goroutine 可能还在运行,新
Add()和旧Done()并发修改同一计数器 → 竞态或负数 - 正确做法:每个任务批次新建一个
var wg sync.WaitGroup,不要重复使用变量 - 需要超时控制?用
context.WithTimeout+select配合wg.Wait(),而不是靠复用 WaitGroup
最易被忽略的点:defer wg.Done() 放在函数开头这件事,看起来 trivial,但在复杂 error 处理路径里,它决定了你到底是在调试逻辑 bug 还是在修复并发原语误用。


















