waitgroup 必须先 add 后 done,add 需在 goroutine 启动前完成且数量严格匹配;不可值传递,须传指针;defer done 要防 panic 提前触发;应结合 context.withtimeout 防止无限等待。

WaitGroup 忘记 Add 就 panic
调用 WaitGroup.Done() 前必须先调用 WaitGroup.Add(1),否则会 panic:「panic: sync: negative WaitGroup counter」。这不是运行时检测逻辑错误,而是直接崩溃——Go 不允许计数器为负。
- 常见错误:在 goroutine 内部才调用
Add(1),导致Wait()可能提前返回,或Done()被多调用 - 正确做法:所有
Add()必须在启动 goroutine 之前完成,且数量与实际启动的协程数严格一致 - 如果协程数动态生成(比如遍历切片),
Add(len(tasks))比逐个Add(1)更安全,避免漏加
WaitGroup 不能被复制,传参要用指针
sync.WaitGroup 是一个含 mutex 和原子计数器的 struct,它不支持值传递。go func(wg sync.WaitGroup) {}(wg) 这种写法会导致子 goroutine 操作的是 wg 的副本,主 goroutine 的 wg 计数器永远不变,Wait() 会永久阻塞。
- 必须传
*sync.WaitGroup,并在 goroutine 内通过wg.Done()修改原始实例 - 框架中常见于 handler 或 middleware 里启多个后台任务,此时要确保传入的是同一个指针,而不是闭包捕获后意外复制
- 注意 defer 里调用
wg.Done()的时机:如果 defer 在 goroutine 入口就注册,但函数中途 panic,Done()仍会执行,可能导致计数器过早归零
WaitGroup + context.WithTimeout 防止无限等待
仅靠 WaitGroup.Wait() 无法应对协程卡死、网络 hang 住等场景,一旦某个 goroutine 不退出,整个流程就卡死。生产环境必须搭配超时控制。
- 不要用
time.AfterFunc或单独起 goroutine 杀掉 Wait,容易竞态;应使用context.WithTimeout包裹整体等待逻辑 - 典型模式:
select { case ,其中 <code>done := make(chan struct{}),在wg.Wait()后 close(done) - 注意:
ctx.Done()触发时,你无法强制终止正在运行的 goroutine,只能放弃等待结果,因此业务逻辑本身也需响应ctx并主动退出
框架里混用 defer 和 WaitGroup 容易漏掉 Done
在 Gin/echo 等框架 handler 中,有人习惯把 wg.Add(1) 和 defer wg.Done() 写在同一层,再起 goroutine —— 这样 Done() 会在 handler 函数返回时立即执行,而非 goroutine 结束时。
- 错误示例:
func handler() { wg.Add(1); defer wg.Done(); go func() { ... }() }→Done()在 goroutine 启动后立刻调,不是结束时 - 正确写法:把
defer wg.Done()放进 goroutine 内部,或者显式在 goroutine 末尾调wg.Done() - 更稳妥的方式是封装工具函数,比如
go runWithDone(&wg, func() { ... }),内部自动处理Done()调用时机
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











