Go无真正全局错误处理,需统一构造可追踪error、显式传播、分层响应;recover仅捕获同goroutine panic,不处理普通error;业务错误应返回error而非panic,goroutine错误须显式收集。

Go 没有真正意义上的“全局错误处理”——所谓“全局”,其实是靠统一构造、显式传播、分层响应来模拟的,不是加个中间件或 recover 就万事大吉。
为什么不能靠 recover 拦住所有错误
很多人想用 recover 做“兜底异常捕获”,但这是误解:recover 只对当前 goroutine 里的 panic 有效,跨 goroutine(比如 http handler 启的子协程、定时任务、消息消费)完全捕不到;而且它不处理普通 error,只管崩溃态。业务中 95% 的错误是返回的 error 值,不是 panic。
-
panic应该只用于程序无法继续的致命错误(如配置加载失败、DB 连接池初始化失败),不是业务错误的出口 - HTTP handler 里写
defer recover看似“安全”,实则掩盖了本该显式处理的业务错误,还可能让日志丢失 traceID 和上下文 - goroutine 错误必须显式收集,例如用
errgroup.Group等待并聚合多个子任务的error
怎么统一构造可追踪的 error
裸用 errors.New("xxx") 或 fmt.Errorf("xxx") 会导致错误无堆栈、无类型、无 traceID,查问题时只能靠猜。
- 一律用
fmt.Errorf("context: %w", err)链式包装,保留原始错误和调用链 - 在入口(如 HTTP middleware)把
trace_id、user_id注入context.Context,再通过自定义 error 类型或日志字段透出 - 避免用字符串匹配判断错误(如
strings.Contains(err.Error(), "timeout")),改用errors.Is(err, context.DeadlineExceeded)或自定义错误类型 +errors.As - 第三方库错误(如
sql.ErrNoRows)应在 DAO 层做一次映射,转成你自己的业务错误类型,别让上层直面底层细节
HTTP handler 怎么做到错误响应格式统一
每个 handler 自己写 if err != nil { writeJSON(w, 500, "failed", err.Error()) } 容易漏、难审计、无法插桩(比如自动上报、熔断统计)。
- 用函数包装器:定义
func(http.ResponseWriter, *http.Request) error类型的 handler,外层套一层WithErrorHandling中间件 - 中间件里统一提取
trace_id、记录耗时、分类错误码(如400对应参数错误,500对应内部错误),再写结构化响应 - 不要把原始
err.Error()直接返回给前端,敏感信息(如 SQL 语句、路径、密码字段名)要过滤或脱敏 - 如果用了 Gin/Echo 等框架,优先用其内置的
AbortWithStatusJSON或CustomRecovery,但注意它们默认不带上下文,仍需手动注入trace_id
goroutine 错误怎么不丢、不静默
启动新 goroutine 时,错误若没被接收或检查,就彻底消失——日志没记录、监控没告警、调用方毫不知情。
- 永远不要写
go doSomething()而不处理它的 error;要么用errgroup.Group等待结果,要么把 error 发到 channel 由主 goroutine 汇总 - 后台任务(如定时同步、消息重试)必须有错误重试策略 + 最大重试次数 + 失败后持久化告警,不能“失败就结束”
- 使用
context.WithTimeout控制子任务生命周期,配合select捕获ctx.Done()并区分是超时还是主动取消 - 日志打点必须包含 goroutine 标识(比如用
log.With("goroutine", "sync-user-job")),否则并发场景下根本分不清哪条日志属于哪个任务
最常被忽略的一点:错误处理不是加个中间件或 recover 就完事,而是从函数签名设计开始——每个可能失败的操作,是否返回 error?是否包装了足够上下文?上层是否真正在意这个 error?还是只是写了个 if err != nil { return err } 就交差?这才是“全局”的起点。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











