当前位置:首页 > 文章列表 > Golang > Go教程 > Go errgroup 实战:并发扇出别把错误和取消弄丢

Go errgroup 实战:并发扇出别把错误和取消弄丢

来源:Go 官方扩展包文档 2026-06-04 15:00:39 0浏览 收藏

我见过很多 Go 服务的并发扇出代码,一开始都写得很豪爽:循环里起 goroutine,外面套一个 sync.WaitGroup,最后等一等就完事。上线之后才发现,某个下游失败了没人管,调用方取消了 goroutine 还在跑,结果切片并发写还偶尔冒数据竞争。代码看起来并发了,实际上只是把问题并发放大了。

这类场景我现在优先看 golang.org/x/sync/errgroup。它解决的不是“怎么启动 goroutine”这么简单,而是把一组 goroutine 的错误、等待、取消信号和并发上限收在一个可读的边界里。官方文档里也把它定位成一组 goroutine 的同步、错误传播和 Context 取消工具。

Go errgroup 并发治理思维导图
思维导图:errgroup 的重点是错误收口、取消传播、并发上限和结果聚合,不只是少写几行 WaitGroup。

事故场景:批量查下游,越并发越不稳

假设一个商品页要并发拉价格、库存、优惠、推荐、风控标签。手写 goroutine 很快,但问题也很快:某个接口失败后,其他 goroutine 还继续跑;上游请求取消后,下游调用照样打;错误只能塞到共享变量里,稍不注意就是 data race。

func LoadPage(ctx context.Context, ids []string) ([]Item, error) {
    var wg sync.WaitGroup
    var items []Item
    var firstErr error

    for _, id := range ids {
        wg.Add(1)
        go func() {
            defer wg.Done()
            item, err := loadItem(ctx, id)
            if err != nil {
                firstErr = err // 并发写,错误也不可靠
                return
            }
            items = append(items, item) // 并发写 slice
        }()
    }

    wg.Wait()
    return items, firstErr
}

这段代码有三个经典坑:循环变量可能用错,结果和错误共享写没有保护,失败后没有取消其他任务。Go 新版本已经修过一类循环变量问题,但生产代码不能只靠语言修补;并发边界还是要自己写清楚。

Go errgroup 工作流程图
流程图:先用 WithContext 建组,再限制并发,任务里返回错误,最后由 Wait 统一收口。

用 errgroup.WithContext 把失败收回来

errgroup.WithContext 会返回一个 Group 和派生 Context。组内任意一个任务返回非 nil 错误时,这个 Context 会被取消。其他任务如果正确传递 ctx,就能尽快退出,不会继续把下游打满。

func LoadPage(ctx context.Context, ids []string) ([]Item, error) {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(8)

    results := make([]Item, len(ids))
    for i, id := range ids {
        i, id := i, id
        g.Go(func() error {
            item, err := loadItem(ctx, id)
            if err != nil {
                return fmt.Errorf("load item %s: %w", id, err)
            }
            results[i] = item
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        return nil, err
    }
    return results, nil
}

这里我用固定下标写结果,而不是多个 goroutine 一起 append。只要每个 goroutine 写不同的位置,这比加锁 append 更简单,也更容易 review。真正需要共享 map 或聚合结构时,再用 mutex 或 channel 收口。

SetLimit:别把并发扇出写成下游压测器

errgroup 的 SetLimit 很适合控制活跃 goroutine 数。比如一次请求里有 200 个 ID,要不要同时打 200 个下游请求?我的答案通常是不。你要看下游容量、连接池、接口 SLA 和调用方预算,而不是看机器还能不能起 goroutine。

并发上限不是越大越好。上限过小会拖慢总耗时,上限过大会把下游、连接池、CPU 和内存一起推高。我一般会先按下游接口延迟和容量估一个保守值,比如 8 或 16,再用压测看 P95、错误率和下游 QPS。

Go errgroup 修复并发扇出案例图
案例图:修复前错误和资源不可控,修复后用 errgroup 统一收口并限制并发。

TryGo:达到上限时不要硬塞任务

TryGo 适合那些“能跑就跑,不能跑就降级”的场景。它在当前活跃 goroutine 达到上限时不会启动新任务,而是返回 false。比如非关键推荐、补充标签、可选预热任务,就可以在繁忙时跳过。

if ok := g.TryGo(func() error {
    return warmupOptionalCache(ctx, key)
}); !ok {
    metrics.Count("warmup.skipped")
}

但核心链路别滥用 TryGo。订单、支付、权限这种任务不能因为 goroutine 满了就默默跳过。TryGo 的价值是明确降级,而不是悄悄丢任务。

Context 取消必须传到底

很多 errgroup 代码表面上用了 WithContext,但任务内部又调用了不接收 ctx 的函数,或者重新用了 context.Background()。这会把取消链路直接切断。第一个任务失败后,Group 的 ctx 已经取消了,可其他任务依然在等 I/O。

我的 review 习惯是从 g.Go 里的函数一路追下去,看数据库、HTTP、RPC、缓存、文件操作有没有吃到同一个 ctx。只要有一段不吃 ctx,就要问清楚为什么。

错误怎么返回才好排查

errgroup 的 Wait 会返回第一个非 nil 错误。这个错误一定要带上下文,否则你只会得到一句 deadline exceededconnection reset,不知道哪个 ID、哪个下游、哪个阶段出了问题。

g.Go(func() error {
    profile, err := userClient.LoadProfile(ctx, uid)
    if err != nil {
        return fmt.Errorf("load profile uid=%d: %w", uid, err)
    }
    profiles[i] = profile
    return nil
})

如果业务需要收集所有错误,errgroup 就不是完整答案。你可以在任务里把错误写入受保护的切片,再返回一个主错误触发取消;也可以不用首错取消,改成 channel 聚合。工具要服务业务语义,不要为了用 errgroup 把需求写歪。

上线前检查清单

  • 是否需要首错取消:一个任务失败后,其他任务继续跑还有没有意义?
  • ctx 是否传到底:HTTP、数据库、RPC、缓存调用有没有接收同一个 ctx?
  • 并发上限是否合理:SetLimit 不能拍脑袋,要结合下游容量和压测。
  • 结果写入是否安全:固定下标写切片、加锁、channel 聚合,三选一写清楚。
  • 错误是否带上下文:错误链里要能看到业务 ID、下游名和动作。
  • 指标是否覆盖:任务数量、并发上限、Wait 耗时、取消次数、首错类型都要能看到。

我自己的经验

errgroup 最适合“同一个请求里有一组相关任务,失败后可以统一取消”的场景。比如聚合页、批量查询、并发校验、并发预加载。它不适合无穷无尽的后台 worker,也不适合需要长期运行的任务编排。

还有一点很现实:errgroup 能让代码更短,但短不是目的。真正的收益是新人读代码时能一眼看懂:这些 goroutine 属于同一组,错误从 Wait 出来,取消从 ctx 传下去,并发由 SetLimit 控住。并发代码能被读懂,才谈得上稳定。

最后聊两句

Go 让启动 goroutine 变得太容易了,所以我们更要克制。并发扇出不是把任务全部丢出去,而是给它们一个边界:谁负责等,谁负责取消,错误从哪里回来,最多同时跑多少,结果怎么安全落地。

如果你的项目里还有手写 WaitGroup 聚合下游请求的代码,建议拿这篇的清单扫一遍。能用 errgroup 收口的地方,尽量收;不能用的地方,也要把错误、取消和并发上限写清楚。线上服务怕的不是 goroutine 少,而是 goroutine 没人管。

版本声明
本文转载于:Go 官方扩展包文档 如有侵犯,请联系study_golang@163.com删除
Python 打包发布实战:别把运行依赖和开发依赖混在一起Python 打包发布实战:别把运行依赖和开发依赖混在一起
上一篇
Python 打包发布实战:别把运行依赖和开发依赖混在一起
MySQL 8.4 函数索引实战:字段一包函数,索引为什么就不走了
下一篇
MySQL 8.4 函数索引实战:字段一包函数,索引为什么就不走了
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ChatExcel酷表:告别Excel难题,北大团队AI助手助您轻松处理数据
    ChatExcel酷表
    ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
    5982次使用
  • Any绘本:开源免费AI绘本创作工具深度解析
    Any绘本
    探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
    6397次使用
  • 可赞AI:AI驱动办公可视化智能工具,一键高效生成文档图表脑图
    可赞AI
    可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
    6207次使用
  • 星月写作:AI网文创作神器,助力爆款小说速成
    星月写作
    星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
    8186次使用
  • MagicLight.ai:叙事驱动AI动画视频创作平台 | 高效生成专业级故事动画
    MagicLight
    MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
    6790次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码