errgroup是带错误传播与协同取消能力的并发协调器,非并发控制器;必须用withcontext初始化,否则goroutine泄漏、超时失效;wait()仅返回首个非nil错误且不捕获panic。

ErrGroup 是什么,为什么不用原生 sync.WaitGroup
Kratos 的 errgroup.Group 本质是带错误传播能力的并发控制结构,比 sync.WaitGroup 更适合微服务聚合场景——它能自动收集第一个非 nil 错误并提前取消其余 goroutine(通过 context.Context),避免“一个失败、全部等完”的低效行为。
常见错误是直接用 sync.WaitGroup + 手动 error 收集:既容易漏 defer Done(),又无法中断已启动但无用的请求,尤其在超时或上游返回 5xx 时浪费资源。
实操建议:
- 总是用
errgroup.WithContext(ctx)初始化,确保上下文可取消 - 不要在 goroutine 内部忽略
err返回值——errgroup只捕获你显式 return 的 error - 如果某个子请求允许失败不影响整体结果,需手动包装为
nil错误(例如降级逻辑)
如何正确传入 context 并设置超时
聚合接口的超时必须由外层统一控制,不能让每个子请求各自设 timeout。Kratos 的 errgroup.Group 依赖传入的 ctx 实现协同取消,一旦任一子 goroutine 报错或超时,其余会收到 ctx.Err() 并退出。
典型错误写法:ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) —— 这样会导致子请求的 context 和主流程无关,cancel 不会触发下游退出。
正确做法:
- 从 handler 入参的
ctx派生子 context:childCtx, cancel := context.WithTimeout(ctx, 2*time.Second) - 把
childCtx传给下游 RPC 客户端(如userClient.GetUser(childCtx, req)),而非原始ctx - 务必调用
defer cancel(),否则可能泄漏 context - 注意:Kratos HTTP transport 默认使用
ctx超时,无需额外配置
怎么处理部分失败的聚合结果
微服务聚合常需“尽力而为”:比如查用户信息失败,但订单和地址还能返回。此时不能让 errgroup.Wait() 直接返回错误阻断整个响应。
关键点在于区分“业务可容忍失败”和“不可恢复错误”:
- 对可降级字段,用
if err != nil { user = &User{}; err = nil }主动清空 error - 对必须成功字段(如鉴权),保持原 error,让
errgroup.Wait()短路返回 - 避免在 goroutine 内 recover panic——
errgroup不捕获 panic,会导致进程崩溃 - 结果结构体建议用指针字段(如
*User),方便区分“未请求”和“请求失败”
示例片段:
var user *v1.User
eg.Go(func() error {
resp, err := userClient.GetUser(ctx, &v1.GetUserRequest{Id: uid})
if err != nil {
// 鉴权失败不可降级
if errors.Is(err, ErrUnauthorized) {
return err
}
// 其他错误降级为空对象
user = &v1.User{}
return nil
}
user = resp.User
return nil
})
性能与内存注意事项
errgroup.Group 本身开销极小,但不当使用会引发 goroutine 泄漏或内存堆积:
- 每个子 goroutine 应尽量轻量——避免在其中做大量计算或阻塞 I/O;耗时逻辑应拆到 service 层异步处理
- 不要在循环里反复 new
errgroup.Group,复用实例无意义且易混淆生命周期 - 如果聚合请求量大(QPS > 1k),考虑加限流(如
gobreaker)防止打垮下游,errgroup不提供熔断能力 - 注意 Kratos 日志默认不打印 goroutine ID,调试并发问题时建议用
runtime.Stack()快速定位卡点
最常被忽略的是 context 生命周期管理:handler 结束后,所有子 goroutine 必须已退出或明确放弃执行,否则会持有 request-scoped 对象(如 DB 连接、logger)导致内存泄漏。











