Uber Go 编码规范:并发编程中的 WaitGroup 使用技巧
在 Go 语言并发编程中,WaitGroup(等待组)是管理 goroutine( goroutine 是 Go 语言中的轻量级线程)生命周期的重要工具。然而,错误使用 WaitGroup 可能导致程序死锁或数据竞争。本文将结合 Uber Go 编码规范,从基础用法到高级技巧,全面解析 WaitGroup 的正确使用方式,帮助开发者避免常见陷阱。
WaitGroup 基础:核心功能与使用原则
WaitGroup 位于 sync 包中,用于等待一组 goroutine 完成。其核心方法包括 Add(delta int)(注册待完成的 goroutine 数量)、Done()(标记单个 goroutine 完成)和 Wait()(阻塞等待所有注册的 goroutine 完成)。Uber 规范强调:必须在启动 goroutine 前调用 Add(),且 Done() 的调用次数必须与 Add() 注册的数量一致。
var wg sync.WaitGroup
// 注册 2 个 goroutine
wg.Add(2)
// 启动第一个 goroutine
go func() {
defer wg.Done() // 确保 goroutine 退出时调用 Done()
// 业务逻辑
}()
// 启动第二个 goroutine
go func() {
defer wg.Done()
// 业务逻辑
}()
wg.Wait() // 等待所有 goroutine 完成
常见错误案例与规范约束
| 错误用法 | 问题描述 | 规范要求 |
|---|---|---|
在 goroutine 内部调用 Add() | 可能导致 Wait() 提前返回 | 必须在启动前注册 |
忘记调用 Done() | 导致 Wait() 永久阻塞 | 使用 defer 确保执行 |
Add() 与 Done() 数量不匹配 | 引发 panic 或死锁 | 严格保持数量一致 |
进阶技巧:优雅管理复杂场景
动态 goroutine 池:限制并发数量
当需要动态创建多个 goroutine 时(如处理任务队列),可结合 channel 与 WaitGroup 实现并发控制。Uber 规范建议:使用带缓冲的 channel 限制同时运行的 goroutine 数量,避免资源耗尽。
func processTasks(tasks []Task, concurrency int) {
var wg sync.WaitGroup
sem := make(chan struct{}, concurrency) // 限制并发数为 concurrency
for _, task := range tasks {
sem <- struct{}{} // 获取信号量,达到上限时阻塞
wg.Add(1)
go func(t Task) {
defer func() {
wg.Done()
<-sem // 释放信号量
}()
// 处理任务 t
}(task)
}
wg.Wait()
close(sem)
}
错误传播:结合 ErrGroup 处理并发错误
Uber 规范指出:若需收集 goroutine 中的错误,应使用 sync.WaitGroup 配合错误通道,或直接使用 golang.org/x/sync/errgroup(扩展库,支持错误传播)。以下是基于原生 WaitGroup 的错误收集方案:
func fetchResources(urls []string) ([]Result, error) {
var wg sync.WaitGroup
results := make([]Result, len(urls))
errs := make(chan error, len(urls)) // 缓冲大小与任务数一致
for i, url := range urls {
wg.Add(1)
go func(idx int, u string) {
defer wg.Done()
res, err := fetch(u)
if err != nil {
errs <- fmt.Errorf("fetch %s: %w", u, err)
return
}
results[idx] = res
}(i, url)
}
// 单独 goroutine 等待所有任务完成后关闭错误通道
go func() {
wg.Wait()
close(errs)
}()
// 收集错误
var err error
for e := range errs {
if err == nil {
err = e
} else {
err = fmt.Errorf("%v; %w", err, e)
}
}
return results, err
}
避坑指南:Uber 规范中的关键约束
禁止在循环中共享循环变量
直接在循环中启动 goroutine 并引用循环变量,可能导致变量值被覆盖。Uber 规范要求:通过函数参数传递循环变量,确保每个 goroutine 获得独立副本。
// 错误示例:所有 goroutine 可能共享同一 i 值
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(i) // 危险:i 可能已被修改
}()
}
// 正确示例:通过参数传递
for i := 0; i < 5; i++ {
wg.Add(1)
go func(num int) {
defer wg.Done()
fmt.Println(num) // 安全:num 为当前 i 的副本
}(i)
}
避免在 WaitGroup 等待期间修改状态
Uber 规范强调:Wait() 仅阻塞等待,不应依赖其返回值判断执行结果。状态修改需通过 channel 或互斥锁同步,例如使用 sync.Mutex 保护共享数据:
var (
wg sync.WaitGroup
mu sync.Mutex // 保护结果切片
results []int
)
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
res := compute()
mu.Lock()
results = append(results, res) // 互斥锁确保安全
mu.Unlock()
}()
}
wg.Wait()
最佳实践总结
- 初始化与声明:使用
var wg sync.WaitGroup而非指针(零值可用,规范链接)。 - 资源清理:始终通过
defer wg.Done()确保Done()被调用(规范链接)。 - 并发控制:结合 channel 实现 goroutine 池,避免无限制创建(规范链接)。
- 错误处理:使用带缓冲的 error channel 或
errgroup收集并发错误(规范链接)。
通过遵循上述规范与技巧,开发者可有效避免 WaitGroup 使用中的常见问题,编写更健壮的 Go 并发程序。完整规范细节可参考 Uber Go 编码规范文档。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



