errgroup 语义实现¶
要点卡¶
30 秒回答
errgroup = WaitGroup + 首个 error + Context 取消。WithContext 派生可取消 ctx;任一 Go(fn) 返回 error → errOnce 记录 + cancel;Wait 等全部结束并返回该 error。讲解关键词:errOnce、cancel 传播、Wait 后再 cancel 一次无害。
3 分钟展开
- 是什么:
golang.org/x/sync/errgroup简化「多 goroutine 有一失败则全员停工」。 - 为什么:并行调多个 RPC/查多个库,一个失败不应继续浪费资源(见 S-CONC-17 Pipeline)。
- 怎么做:
WaitGroup计数;Go里defer Done();error 时errOnce.Do存 err 并cancel();子任务select ctx.Done()。
| 记忆槽 | 内容 |
|---|---|
| 三个不变量 | 首个非 nil error 触发派生 ctx 取消;Wait 仍等待所有已启动任务;任务必须主动观察取消 |
| 手画图 | Go A/B/C → first error → cancel ctx → cooperative exits → Wait → first error |
| 项目落点 | 用实际并行 RPC、链节点查询或批量校验说明 fail-fast 与下游请求取消;只引用本人实际参与部分和可解释指标 |
| 一个取舍 | fail-fast 节省资源但可能丢失完整错误集合;需要逐项结果时应换聚合语义 |
错误表达
- ❌ “errgroup 一出错就强制杀死其他 goroutine,并立刻从 Wait 返回;还能自动回滚副作用。”
- ✅ “取消是协作信号,Wait 要等任务返回;事务补偿、panic 和部分结果都需业务层另行设计。”
自测追问:函数忽略 ctx 永久阻塞时 errgroup 能做什么?SetLimit 限制的是 active 还是所有排队调用?
10 分钟版(原理 + 图示)¶
flowchart TD
Start[WithContext] --> G1[Go task A]
Start --> G2[Go task B]
G2 -->|error| Cancel[cancel ctx]
Cancel --> G1
G1 -->|ctx.Done| Stop[提前退出]
G1 --> Wait[Wait]
G2 --> Wait
Wait --> Ret[return first err]
与 WaitGroup 对比
| WaitGroup | errgroup | |
|---|---|---|
| 错误 | 需自行 channel/atomic | 内置首个 error |
| 取消 | 需自建 ctx | 内置 cancel |
| 适用 | 纯等待 | 并行任务有失败语义 |
手写要点
type Group struct {
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
errOnce sync.Once
err error
}
errOnce保证只记录 第一个 errorWait()末尾再cancel():清理未触发 error 的路径(与官方实现类似)
生产场景¶
- 启动时并行 ping 多个依赖
- 批量导出:任一分片失败则中止
- 与
gin-example/example_28errgroup 多服务启动同类
排查与工具¶
go test ./errgroup/...- 生产直接用
golang.org/x/sync/errgroup
架构取舍¶
| 方案 | 适用 |
|---|---|
| 手写 errgroup | 编码练习 / 中等负载 |
| x/sync/errgroup | 生产 |
| channel 收 err | 需收集 所有 错误时 |
深挖问答¶
- 为何 errOnce? → 多 goroutine 同时失败,只保留首个有意义。
- 子任务如何感知取消? →
fn内select <-ctx.Done()或传 ctx 给 HTTP/DB。 - SetLimit(n) 呢? → 官方扩展:
Go在达到上限时会阻塞;有任务运行时不能修改 limit。若不希望阻塞提交方可看TryGo。 - Wait 返回 nil 但 ctx 已 cancel? →
Wait只返回任务函数报告的首个 error。若 parent 已取消但所有函数都忽略取消并返回 nil,Wait仍可返回 nil;任务应把ctx.Err()向上返回。
反模式与事故¶
- 官方
errgroup不把 panic 自动转成 error。只应在明确的进程/请求边界按策略 recover、保留堆栈并告警;不要在每个任务里无差别吞掉 panic - 不用 ctx 仍调阻塞 IO → cancel 无效,Wait hung
- 在 Go 外再开 goroutine 不 Wait → 泄漏
代码示例¶
见 examples/senior/errgroup/errgroup.go:
func (g *Group) Go(fn func() error) {
g.wg.Add(1)
go func() {
defer g.wg.Done()
if err := fn(); err != nil {
g.errOnce.Do(func() {
g.err = err
g.cancel()
})
}
}()
}
cd examples/senior && go test ./errgroup/...