

新闻资讯
行业动态WaitGroup 必须先 wg.Add(n) 再启动 goroutine,且 Done() 须在 goroutine 内且仅调用一次;必须传指针避免复制;常与 channel 配合实现结果收集;复杂场景推荐 errgroup.Group。
很多人写成先 go func() { ... wg.Done() }() 再 wg.Add(1),这会导致 Wait() 永远不返回,甚至 panic:「panic: sync: negative WaitGroup counter」。因为 Add 和 Done 不是原子配对操作,Done 可能早于 Add 执行。
正确顺序是:先调用 wg.Add(n),再启动 goroutine;每个 goroutine 结束前必须且只能调用一次 wg.Done()。
wg.Add(1) 必须在 go 语句之前,或至少确保在任何 Done() 调用前完成wg.Add(n) 可一次性加总数,不必循环里每次加 1(但加总值必须准确)Done(),也不要在同一个 goroutine 里调用多次Go 中 sync.WaitGroup 是含 mutex 字段的结构体,直接值传递会复制锁状态,导致未定义行为——常见现象是 Wait() 卡死、或 panic:「sync: WaitGroup is reus
ed before previous Wait has returned」。
所有跨 goroutine 共享的 WaitGroup 实例,必须以 *sync.WaitGroup 形式传递。
wg *sync.WaitGroup,调用时传 &wg
wg 时,如果该闭包会被 go 启动,也要确保是引用原变量(即变量本身是地址可取的,比如局部变量或字段)go work(wg)(wg 是值),而应写 go work(&wg)
仅靠 wg.Wait() 只能等全部完成,无法获知中间结果、错误或进度。实际开发中常配合 channel 使用:
var wg sync.WaitGroup results := make(chan string, 10) errors := make(chan error, 5)for _, url := range urls { wg.Add(1) go func(u string) { defer wg.Done() resp, err := http.Get(u) if err != nil { errors <- err return } results <- resp.Status }(url) }
go func() { wg.Wait() close(results) close(errors) }()
// 非阻塞收结果 for r := range results { fmt.Println("OK:", r) }
WaitGroup 管生命周期,channel 管数据流,职责分离wg.Wait() 后关闭 channel,否则 range 会永远阻塞errors channel + 单独 goroutine 关闭当任务可能失败且需要「任一错误即终止」或「收集全部错误」时,sync.WaitGroup 就显得笨重。golang.org/x/sync/errgroup 是更现代的选择:
g, _ := errgroup.WithContext(ctx)
for _, task := range tasks {
task := task // 避免循环变量复用
g.Go(func() error {
return doWork(task)
})
}
if err := g.Wait(); err != nil {
log.Fatal(err) // 任意一个 goroutine 返回非 nil error,Wait 就返回它
}
errgroup.Group 自动处理 goroutine 启动和等待,无需手动 Add/Done
WaitGroup 本身很简单,但它的使用边界非常明确:只负责“计数等待”,不处理错误、不传递数据、不响应取消。一旦需求超出这个范围,硬套 WaitGroup 容易写出难调试的竞态或死锁。