

新闻资讯
技术教程WaitGroup 是用于等待多个 goroutine 完成的同步原语,必须在启动 goroutine 前调用 Add(),goroutine 内用 defer wg.Done(),主线程调用 Wait();不可复制、需传指针,常与 context.WithTimeout 配合防死锁。
当多个 goroutine 并发执行、且主 goroutine 需要等待它们全部结束才能继续时,sync.WaitGroup 是最直接可靠的同步手段。它不是用来保护共享数据的(那是 sync.Mutex 的事),而是用来“计数+阻塞等待”的。如果你写了 go func() { ... }() 但没等它们跑完就退出了,程序大概率提前结束——这时你就需要 WaitGroup。
WaitGroup 必须在启动 goroutine 前调用 Add(),且不能在 goroutine 内部调用

Add() 后再调用 Done() ——除非你明确控制好竞态(不推荐)。常见错误是把 Add() 放在 go 语句之后,导致 Wait() 立即返回(因为计数还没加)或 panic(Add 被并发调用)。
Add() 必须在 go 语句之前,或至少确保在任何 Done() 之前被调用一次Done() 应该在 goroutine 结束前调用,通常放在函数末尾或 defer 中Wait() 只能在主线程(或需等待的 goroutine)中调用,且只能等一次;重复调用不会报错但无意义package mainimport ( "fmt" "sync" "time" )
func main() { var wg sync.WaitGroup
for i := 0; i < 3; i++ { wg.Add(1) // ✅ 必须在 go 前 go func(id int) { defer wg.Done() // ✅ 推荐用 defer,确保执行 fmt.Printf("goroutine %d running\n", id) time.Sleep(time.Second) }(i) } wg.Wait() // ✅ 主线程阻塞等待全部完成 fmt.Println("all done")}
WaitGroup 不能跨协程复用,也不能复制传递
sync.WaitGroup是一个包含 mutex 和 counter 的结构体,它**不可拷贝**。如果你把它作为参数传给函数并试图在函数内修改(比如调用Add()),又或者在多个地方赋值(wg2 := wg),Go 编译器会报错:cannot assign to struct containing sync.WaitGroup或运行时报 data race。必须传指针,且所有操作都应针对同一个地址。
*sync.WaitGroup,而不是 sync.WaitGroup
WaitGroup.Wait() 是永久阻塞的,一旦某个 goroutine 卡住或死锁,整个程序就卡死。生产环境强烈建议加超时控制。常见做法是用 context.WithTimeout 启动一个 select 监听 done 通道和超时信号。
package mainimport ( "context" "fmt" "sync" "time" )
func main() { var wg sync.WaitGroup ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel()
wg.Add(1) go func() { defer wg.Done() time.Sleep(5 * time.Second) // 故意超时 fmt.Println("this won't print") }() done := make(chan struct{}) go func() { wg.Wait() close(done) }() select { case <-done: fmt.Println("all finished") case <-ctx.Done(): fmt.Println("timeout waiting for goroutines") }}
真正容易被忽略的是:WaitGroup 本身不提供取消能力,也不感知上下文取消。它的职责就是计数,超时、中断、错误传播这些都得靠外层机制补足。别指望靠
WaitGroup解决所有并发协调问题。