Go 等待组:sync.WaitGroup 与同步
在 Go 语言中,并发是核心特性之一。你经常需要启动多个 goroutine(轻量级线程)来并行处理任务,但主程序不能提前退出——否则后台 goroutine 会被强制终止。sync.WaitGroup 就是用来解决这个问题的标准工具:它让你能“等待”一组 goroutine 全部完成后再继续执行。
理解 WaitGroup 的基本原理
sync.WaitGroup 内部维护一个计数器。你通过以下三个方法操作它:
Add(delta int):增加(或减少)计数器的值。Done():将计数器减 1(等价于Add(-1))。Wait():阻塞当前 goroutine,直到计数器归零。
关键规则:必须在启动 goroutine 之前调用 Add(),否则可能因竞态条件导致程序崩溃或逻辑错误。
基础使用步骤
-
导入
sync包:import "sync" -
声明一个
WaitGroup变量:var wg sync.WaitGroup -
在启动每个 goroutine 前,调用
wg.Add(1):wg.Add(1) go func() { defer wg.Done() // 确保函数退出时计数器减 1 // 执行实际任务 }() -
在所有 goroutine 启动后,调用
wg.Wait()阻塞主流程:wg.Wait()
完整示例:并发打印
下面代码启动 3 个 goroutine,每个打印一个数字,主程序等待它们全部完成后才结束:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 每次循环前增加计数
go func(id int) {
defer wg.Done()
time.Sleep(time.Millisecond * 100) // 模拟耗时操作
fmt.Printf("Goroutine %d finished\n", id)
}(i)
}
wg.Wait() // 等待所有 goroutine 完成
fmt.Println("All done!")
}
运行结果(顺序可能不同):
Goroutine 2 finished
Goroutine 1 finished
Goroutine 3 finished
All done!
常见错误与避坑指南
错误 1:在 goroutine 内部调用 Add()
// ❌ 危险!可能导致 Wait() 提前返回
go func() {
wg.Add(1) // 错误位置
defer wg.Done()
// ...
}()
wg.Wait()
后果:如果 Wait() 在 Add(1) 执行前被调用,计数器为 0,Wait() 立即返回,主程序退出,goroutine 被杀死。
正确做法:始终在启动 goroutine 之前调用 Add()。
错误 2:忘记调用 Done()
// ❌ 计数器永远不会归零,程序永久阻塞
wg.Add(1)
go func() {
// 忘记 wg.Done()
}()
wg.Wait() // 死锁!
解决方案:使用 defer wg.Done() 确保无论函数如何退出(包括 panic),计数器都会减少。
错误 3:负计数器
// ❌ panic: sync: negative WaitGroup counter
wg.Add(-1)
原因:Add() 的参数不能使计数器变为负数。即使传入负值,也必须保证总和非负。
建议:除非你明确知道自己在做什么,否则只对 Add() 传正整数。
高级技巧:动态任务数量
有时你不知道要启动多少 goroutine(例如从队列消费任务)。此时可结合通道(channel)与 WaitGroup:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
tasks := []string{"A", "B", "C"}
taskChan := make(chan string, len(tasks))
for _, t := range tasks {
taskChan <- t
}
close(taskChan)
// 启动固定数量的工作 goroutine
numWorkers := 2
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for task := range taskChan {
fmt.Printf("Processing %s\n", task)
}
}()
}
wg.Wait()
fmt.Println("All tasks processed")
}
此模式适用于“生产者-消费者”场景,WaitGroup 确保所有工作 goroutine 处理完通道中的任务后才退出。
WaitGroup 与其他同步机制对比
| 机制 | 适用场景 | 是否阻塞 | 是否可重用 |
|---|---|---|---|
sync.WaitGroup |
等待固定/动态数量的 goroutine 完成 | 是(Wait() 阻塞) |
否(计数器归零后不可再用) |
channel |
goroutine 间通信、信号传递 | 可选(带缓冲/无缓冲) | 是 |
sync.Once |
确保某段代码仅执行一次 | 否 | 是(但只触发一次) |
选择建议:
- 如果只是“等所有任务结束”,用
WaitGroup最简单直接。 - 如果需要传递数据或复杂协调,优先考虑 channel。
性能与注意事项
WaitGroup的操作是原子的,线程安全,无需额外加锁。Wait()可被多个 goroutine 同时调用,它们都会在计数器归零时被唤醒。- 不要复用已归零的 WaitGroup。若需重复使用,应创建新的实例。
// ❌ 不要这样做
var wg sync.WaitGroup
wg.Add(1)
go func() { wg.Done() }()
wg.Wait()
wg.Add(1) // 这里可能 panic 或行为未定义
// ...
// ✅ 正确做法:重新声明或赋值
wg = sync.WaitGroup{}
wg.Add(1)
// ...
实战:并发下载文件
假设你要并发下载多个 URL,并确保全部下载完成后再继续:
package main
import (
"fmt"
"net/http"
"sync"
)
func download(url string, wg *sync.WaitGroup) {
defer wg.Done()
resp, err := http.Get(url)
if err != nil {
fmt.Printf("Error downloading %s: %v\n", url, err)
return
}
defer resp.Body.Close()
fmt.Printf("Downloaded %s, status: %s\n", url, resp.Status)
}
func main() {
urls := []string{
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
}
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go download(url, &wg)
}
wg.Wait()
fmt.Println("All downloads completed")
}
注意:这里将 *sync.WaitGroup 作为参数传给 goroutine 函数,避免闭包捕获问题。
调试技巧:检测 WaitGroup 泄漏
如果程序卡在 Wait() 不动,很可能是某个 goroutine 没有调用 Done()。可通过以下方式排查:
- 添加日志:在每个 goroutine 开头和
Done()前打印标识。 - 设置超时:结合
context或time.After避免永久阻塞。 - 使用
-race标志编译:检测潜在的数据竞争。
例如,带超时的等待:
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
// 正常完成
case <-time.After(5 * time.Second):
fmt.Println("Timeout! Some goroutines may be stuck.")
}
最佳实践总结
- 总是在启动 goroutine 前调用
wg.Add(1)。 - 总是在 goroutine 内部使用
defer wg.Done()。 - 不要对同一个
WaitGroup实例重复使用(归零后重建)。 - 优先传递指针(
*sync.WaitGroup)给函数,避免值拷贝导致计数器不一致。 - 结合 channel 或 context 处理超时和取消逻辑,避免死锁。
// ✅ 推荐写法模板
var wg sync.WaitGroup
for item := range items {
wg.Add(1)
go func(x Item) {
defer wg.Done()
process(x)
}(item)
}
wg.Wait()
暂无评论,快来抢沙发吧!