在Go语言中,sync.WaitGroup(简称WaitGroup)是用于多goroutine同步的核心机制,但其使用需谨慎,否则可能导致程序卡顿、死锁或数据竞争等问题。以下是关键踩坑点及解决方案:
- 未启动单独goroutine导致主线程阻塞
问题:若WaitGroup未在goroutine内调用,主线程可能因等待子任务完成而阻塞。
解决方案:确保WaitGroup操作在独立goroutine中执行:
go
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// 子任务逻辑
}()
wg.Wait() // 主线程等待子任务完成
- 计数器操作顺序错误
问题:Add(n)应在goroutine启动前调用,否则可能导致Wait()提前返回。
解决方案:遵循Add() -> 启动goroutine -> Done()的顺序:
go
wg.Add(1) // 增加计数器
go func() {
defer wg.Done() // 任务结束时减少计数器
// 任务逻辑
}()
- 闭包变量捕获问题
问题:从循环启动goroutine时,若闭包捕获循环变量(如for循环中的i),可能导致所有goroutine共享同一变量值。
解决方案:通过参数传递循环变量值:
go
for i := 0; i < 5; i++ {
wg.Add(1)
go func(num int) { // 传递参数
defer wg.Done()
fmt.Println("Goroutine", num)
}(i) // 传递当前i值
}
- 未正确传递指针导致计数器失效
问题:将WaitGroup作为值传递给函数时,子函数操作的是副本,主函数计数器不变。
解决方案:传递指针引用:
go
func worker(wg *sync.WaitGroup) {
defer wg.Done()
// 任务逻辑
}
wg.Add(1)
worker(&wg) // 传递指针
- 未调用Done()导致死锁
问题:若goroutine未调用Done(),计数器永远不会归零,Wait()将永久阻塞。
解决方案:确保每个goroutine结束前调用Done():
go
go func() {
defer wg.Done() // 确保Done()在return前执行
// 任务逻辑
}()
- 并发场景下的资源泄漏
问题:若WaitGroup未正确管理goroutine生命周期,可能导致资源泄漏。
解决方案:结合context实现超时控制:
go
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
go func(ctx context.Context) {
select {
case <-ctx.Done():
return // 超时退出
case <-time.After(2 * time.Second):
// 任务逻辑
}
}(ctx)
示例代码
go
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(1 * time.Second)
fmt.Println("Task completed")
}()
wg.Wait() // 等待任务完成
WaitGroup是Go并发编程的基石,但需严格遵循计数器操作顺序和闭包变量传递规则,避免常见陷阱。