errgroup.go() 和 gocontext() 中必须为每个 goroutine 单独添加 recover,因其 panic 不被 wait() 捕获;recover 需置于函数首 defer,早于 ctx.err() 检查;子 errgroup 也需各自 recover;panic 转 error 应用 fmt.errorf("panic: %s", fmt.sprint(r)) 保结构。

errgroup.Go() 里不加 recover 就会 panic 崩溃
errgroup 的 Wait() 完全不捕获 panic —— 只要任意一个 Go() 或 GoContext() 闭包里触发 panic,整个程序立刻退出,根本不会走到 Wait()。这不是 bug,是设计使然:errgroup 只管 error 传播,不管 panic。
常见错误现象:解析第三方返回的 JSON 字段时没做类型断言检查,json.Unmarshal 后直接 .(*User).Name,上游返回了 null,panic “invalid memory address”,服务进程挂掉。
- 每个传给
Go()或GoContext()的函数,都必须自己包一层defer func() { ... }() - recover 后不能只 log 然后 return nil,否则错误被吞掉;必须显式 return 一个
error - 别在 defer 里直接
log.Fatal或os.Exit,那会绕过 errgroup 的错误聚合逻辑
GoContext 闭包里 recover 要早于 ctx.Done() 检查
用 GoContext() 时,典型结构是:func(ctx context.Context) error { defer recover(); if ctx.Err() != nil { return ctx.Err() }; ... }。但顺序错了就白写。
如果先做耗时操作(比如 http.Client.Do),再 recover,panic 发生时 ctx 可能已超时,但你已经错过了 cancel 信号响应窗口;更糟的是,recover 放在最后,panic 一发生就终止函数,ctx.Done() 根本没机会检查。
- recover 必须放在函数最开头的 defer 中(即第一个 defer)
- ctx.Err() 检查要放在业务逻辑之前,且不能被 recover 捕获遮蔽(recover 只捕获 panic,不影响 error 返回)
- 正确模板:
g.GoContext(func(ctx context.Context) error { defer func() { if r := recover(); r != nil { // 转成 error,避免崩溃 } }() if err := ctx.Err(); err != nil { return err } // 正常业务逻辑 })
嵌套 errgroup 时 recover 不能漏层级
主流程用 errgroup 拉用户列表,每个用户再启一个子 errgroup 拉订单和地址——这种嵌套很常见。但子组里的 panic 如果没 recover,崩的不是子组,而是整个进程。
错误做法:只在顶层 errgroup 的 GoContext() 里加 recover,认为“父层兜底就行”。实际上 recover 是 goroutine 级别的,子 errgroup 启动的新 goroutine 属于新执行流,父层 defer 完全无效。
- 每个独立的
Go()/GoContext()调用点,都要有自己的 recover - 子 errgroup 的初始化位置(比如
childEg, _ := errgroup.WithContext(childCtx))之后,所有传给它的函数也得各自加 defer recover - 不要试图在子组
Wait()外层加 recover——它不运行在子 goroutine 里,捕不到子任务 panic
recover 转 error 时别用 fmt.Errorf("%v", r)
把 panic 值转成 error 时,用 fmt.Errorf("panic: %v", r) 看似省事,但会切断错误链,导致上层无法用 errors.Is(err, xxx) 判断类型,日志里也看不到原始 panic 类型(比如 runtime.errorString)。
更麻烦的是,如果 r 是自定义 panic 类型(如 type FatalErr struct{...}),%v 只输出字符串,丢失结构信息。
- 优先用
fmt.Errorf("panic: %w", r),前提是 r 实现了error接口(很多 panic 值不是) - 稳妥做法:
fmt.Errorf("panic: %s", fmt.Sprint(r)),保留可读性又不破坏 error 接口 - 若需区分 panic 类型,可在 recover 后做类型断言:
if e, ok := r.(error); ok { return e }
Go(),都得当成一个可能独自崩溃的微型进程来防护。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











