标签: errgroup

  • Go 并发控制三种武器:WaitGroup、errgroup 与 semaphore

    Go 并发控制三种武器:WaitGroup、errgroup 与 semaphore

    为什么需要并发控制

    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 负责,别把三个职责混在一个原语里硬凑。