goroutine 中 panic 仅终止自身,不传播至父 goroutine;须在每个 goroutine 内用 defer+recover 捕获并记录,或改用 channel 返回 error,errgroup 可简化并发错误处理与上下文取消。

goroutine 中 panic 会自动终止,但不会传播到父 goroutine
Go 的 goroutine 是独立的执行单元,panic 发生后仅终止当前 goroutine,主 goroutine 或其他 goroutine 完全无感。这看似“安全”,实则掩盖错误——比如一个后台日志上传 goroutine 因 http.Post 失败而 panic,程序照常运行,日志却悄然丢失。
常见错误现象:
- 程序无报错、无日志、功能部分失效(如定时任务停摆)
- go run 不输出 panic 信息(因未被捕获且未关联 stdout)
- 使用 recover 却放在错误位置(如放在主函数而非 goroutine 内部)
- 必须在每个可能 panic 的 goroutine 内部用
defer + recover捕获,不能依赖外部 - 不要把
recover()放在启动 goroutine 的函数里——它只对当前 goroutine 生效 - 捕获后建议记录错误(至少
log.Printf),否则等于静默丢弃
通过 channel 传递 error 值比 panic 更可控
当 goroutine 承担明确任务(如读文件、调 API、处理单条消息),应优先返回 error 而非触发 panic。channel 是最自然的错误传递载体,尤其配合结构体封装结果与错误。
使用场景:
- 启动多个子任务并等待全部完成(如并发请求多个微服务)
- 需区分“任务失败”和“goroutine 崩溃”两类问题
- 要求调用方决定是否重试或降级
- 定义结果类型:例如
type Result struct { Data interface{}; Err error } - goroutine 执行完写入 channel:
ch - 主 goroutine 用
select或循环接收,检查每个Result.Err - 避免无缓冲 channel 导致 goroutine 永久阻塞——务必配超时或用带缓冲 channel
context.Context 是跨 goroutine 错误传播与取消的基础设施
context.Context 本身不传 error,但它让 goroutine 能感知“该停了”,从而主动退出并释放资源。这是对错误响应的前置控制——比如上游请求已超时,下游再继续处理 HTTP 请求就毫无意义。
参数差异:
- context.WithTimeout:指定绝对截止时间,到期自动发 cancel
- context.WithCancel:手动调 cancel() 触发,适合错误连锁响应
- context.WithDeadline:与 timeout 类似,但用具体时间点
- 所有接受
context.Context的 Go 标准库函数(如http.Do、time.AfterFunc)都会在 context Done 时返回 error - 自定义 goroutine 必须监听
ctx.Done()并及时 return,否则 context 失效 - 不要把 error 存进 context.Value —— 这违反 context 设计初衷,且无法被下游感知
errgroup.Group 提供简洁的并发错误收集模式
golang.org/x/sync/errgroup 是官方维护的扩展包,解决“多个 goroutine 中任意一个出错就整体失败”的典型需求。它内部用 channel + context 封装,比手写更可靠。
性能 / 兼容性影响:
- 无额外 CGO 依赖,纯 Go 实现,可直接 go get
- 启动 goroutine 数量大时(>1000),比手写 channel select 略轻量(复用底层 sync.Pool)
- Go 1.21+ 支持 errgroup.WithContext,自动继承 cancel 行为
import "golang.org/x/sync/errgroup"func fetchAll(urls []string) error { g, ctx := errgroup.WithContext(context.Background()) results := make([]string, len(urls))
for i, url := range urls { i, url := i, url // 避免闭包变量复用 g.Go(func() error { req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) results[i] = string(body) return nil }) } if err := g.Wait(); err != nil { return err // 第一个非 nil error } return nil}
错误真正难处理的地方不在语法,而在权衡:要不要让一个 goroutine 的失败拖垮整个请求?要不要等所有子任务都试一遍再报错?这些决策藏在
errgroup的 “第一个错误就返回” 和自定义 channel 收集全部结果之间,也藏在context.WithTimeout的秒级精度和业务实际容忍度之间。










