waitgroup 作为结构体字段时安全,但值传递会因 mutex 不可复制导致死锁或 panic;必须用指针传递和指针接收者,add/done 调用顺序错误会 panic,可重用但需确保 wait 已返回。

WaitGroup 作为结构体字段时,只要不被复制,就安全;但一旦参与函数参数传递或赋值,就极大概率触发死锁或 panic。
WaitGroup 值传递直接导致 Wait 永远阻塞
Go 中 sync.WaitGroup 内部含 sync.Mutex 字段,而 sync.Mutex 不可复制。值传递会让每个副本拥有独立的 mutex 和计数器,Done() 在副本上调用,Wait() 却在原始实例上等待——计数器永远不归零。
- go vet 会立刻报错:
passes sync.WaitGroup by value - 常见错误写法:
func handle(wg sync.WaitGroup)(参数是值) - 正确写法必须是:
func handle(wg *sync.WaitGroup)(参数是指针) - 结构体中嵌入
sync.WaitGroup本身没问题,但若该结构体被复制(如传参、赋值、切片 append),就会连带复制 WaitGroup,风险立即出现
结构体里放 WaitGroup,但方法接收者必须是指针
如果 WaitGroup 是结构体字段,所有操作它的方法都必须使用指针接收者,否则调用时会隐式复制整个结构体,包括 WaitGroup。
- 错误示例:
func (s Service) Start() { s.wg.Add(1) }→s是副本,s.wg也是副本 - 正确写法:
func (s *Service) Start() { s.wg.Add(1) }→ 显式操作原结构体字段 - 即使结构体只读取
wg状态(比如打日志),也建议用指针接收者,避免无意复制 - 初始化结构体时,
wg字段无需显式初始化:sync.WaitGroup{}是零值,可直接用
Add 和 Done 的调用时机错位引发 panic
计数器为负时,Done() 会 panic:sync: negative WaitGroup counter。这和是否指针无关,而是逻辑顺序问题。
-
Add()必须在go启动前完成,且不能被条件跳过 - 禁止在 goroutine 内部先
Done()再Add()(哪怕只差毫秒) - 并发启动多个 goroutine 时,推荐一次性
wg.Add(n),而不是循环中多次Add(1)(除非循环本身是串行的) - 闭包捕获循环变量(如
for i := range jobs { go func() { wg.Done() }() })会导致 Done 被多调或漏调,应显式传参:go func(i int) { defer wg.Done() }(i)
WaitGroup 可重用,但重用前必须确保 Wait 已返回
很多人误以为 WaitGroup 是“一次性”的,其实它支持安全重用——前提是上一轮 Wait() 已返回,且下一轮 Add() 在任何 Done() 之前执行。
- 重用场景常见于:周期性任务调度、连接池中的 worker 复用、测试中多轮并发验证
- 重用时无需重置或新建 WaitGroup,直接
wg.Add(n)即可 - 但要注意:如果某次
Wait()被中断(如 context cancel),而部分 goroutine 还没调Done(),后续重用会导致计数器残留,最终阻塞 - 因此,生产环境建议搭配 context 控制超时,并在 goroutine 内检查 cancel 信号,避免
Done()被遗漏
最易被忽略的一点:WaitGroup 的安全性完全依赖程序员对「谁在哪儿 Add / Done / Wait」的精确控制。它不自动跟踪 goroutine 生命周期,也不做运行时校验——写错就是死锁或 panic,没有中间态。结构体字段化之后,这种责任只会被封装得更隐蔽,而不是被消除。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











