为什么需要并发控制
Go 的 goroutine 很便宜,但便宜不等于可以无节制地起。真实业务里并发任务通常有三类约束:要等所有任务跑完(WaitGroup)、任务之间要共享错误并快速取消(errgroup)、还要限制同时跑的数量(semaphore)。这三个原语分别解决「等齐」「出错即停」「限量」三个不同的问题。
一、WaitGroup:等我的人都到齐
最朴素的同步原语。Add 登记任务数,每个 goroutine 退出前 Done,Wait 阻塞直到计数归零。
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
fetch(u)
}(url)
}
wg.Wait() // 全部完成才继续
坑点:Add 必须在启动 goroutine 之前调用,否则可能在 Wait 已返回后才 Add,直接 panic。循环变量要用参数传入,别在闭包里直接捕获。
二、errgroup:出错即停,错误传递
WaitGroup 只能等齐,不能处理错误,也不能在其中一个任务失败时主动取消其他任务。errgroup 在 WaitGroup 之上加了 context 取消和错误传播能力。
g, ctx := errgroup.WithContext(context.Background())
for _, url := range urls {
url := url
g.Go(func() error {
return fetchWithCtx(ctx, url) // 任一返回 error,ctx 即被取消
})
}
if err := g.Wait(); err != nil {
log.Fatal(err) // 拿到第一个非 nil 错误
}
关键点:g.Go 里启动的函数必须监听 ctx 并支持取消,否则「出错即停」只是空话。需要限制并发数时,配合 semaphore 使用(见下)。
三、semaphore:给并发装上节流阀
errgroup 默认不限并发,瞬间起几百个 goroutine 可能打垮下游。用 golang.org/x/sync/semaphore 的加权信号量,限制同时运行的任务数。
sem := semaphore.NewWeighted(int64(10)) // 最多 10 个并发
g, ctx := errgroup.WithContext(ctx)
for _, job := range jobs {
if err := sem.Acquire(ctx, 1); err != nil {
break
}
j := job
g.Go(func() error {
defer sem.Release(1)
return do(j)
})
}
g.Wait()
三者怎么选
| 原语 | 解决的核心问题 | 能处理错误吗 |
|---|---|---|
| WaitGroup | 等所有任务跑完 | 不能 |
| errgroup | 等齐 + 出错即停 + 错误传递 | 能 |
| semaphore | 限制并发数量(节流) | 需配合 errgroup |
组合才是常态
生产代码里三者几乎总是组合使用:errgroup 管生命周期与错误,semaphore 管并发度,WaitGroup 作为底层机制被 errgroup 封装。记住一句原则——并发数量由 semaphore 决定,错误与取消由 errgroup 负责,别把三个职责混在一个原语里硬凑。
发表回复