Go中无全局异常处理,应严格区分error与panic:error用于可预期错误,panic仅用于初始化失败或不可恢复崩溃;需用errors.Is/As做类型判断,HTTP中间件recover仅兜底意外panic并记录完整堆栈。

Go 里没有“全局异常处理”的概念,所谓“优雅设计”,本质是分清 error 和 panic 的职责边界,再在合适层级做收敛和包装。
别把 panic 当成错误处理入口
很多团队一上来就想用 recover 拦住所有“异常”,结果把 I/O 失败、参数校验不通过、数据库查不到记录这些本该返回 error 的场景全塞进 panic,导致:
- 调用方无法用
if err != nil判断,只能靠日志排查,链路断裂 -
recover吞掉 panic 后,原始错误上下文(比如哪一行、哪个 goroutine)大概率丢失 - HTTP handler 中一旦
panic被 recover,状态码默认是 500,但你其实想返回 400 或 404
真正该用 panic 的只有两类:程序初始化失败(如配置加载失败、端口被占)、不可恢复的逻辑崩溃(如空指针解引用)。其余一律走 error 返回路径。
用 errors.Is 和 errors.As 替代字符串匹配
模块内部定义错误时,别只用 fmt.Errorf("user not found"),否则上层无法区分“用户不存在”和“DB 连接超时”。推荐做法:
立即学习“go语言免费学习笔记(深入)”;
- 定义哨兵错误:
var ErrUserNotFound = errors.New("user not found") - 或封装结构体错误(带码、带上下文):
type AppError struct { Code int; Message string; Cause error },并实现Unwrap()方法 - 调用方统一用
errors.Is(err, ErrUserNotFound)判断类型,用errors.As(err, &e)提取字段
这样模块升级时,只要不改哨兵变量名或结构体字段语义,下游无需改代码。
HTTP 中间件里的 recover 只做兜底,不替代业务错误处理
在 Gin/echo 等框架中加一层 recover 中间件没问题,但必须满足两个条件:
- 只捕获真正意外的 panic(比如模板渲染时传了 nil,或第三方库内部 panic),不是业务逻辑主动
panic - recover 后必须记录完整堆栈(
debug.Stack()),并返回明确状态码,比如:c.AbortWithStatusJSON(500, gin.H{"code": "INTERNAL_ERROR", "msg": "server panic"})
切忌在中间件里 log.Printf("%v", r) 就完事——没堆栈、没 traceID、没请求上下文,等于没记。
模块初始化失败必须提前暴露,不能靠运行时 recover
比如一个 config 模块依赖环境变量,若缺失关键字段,应该在 init() 或 LoadConfig() 函数里直接 log.Fatal(err),而不是等第一次调用才 panic。原因很简单:
- main 函数启动阶段就失败,进程退出,运维能立刻感知
- 避免服务起来后部分功能可用、部分不可用,造成更难定位的“偶发失败”
-
recover对 init 阶段的 panic 无效,它只对运行时 goroutine 生效
复杂模块的错误边界往往不在代码行,而在初始化时机和调用链起点——这点最容易被忽略。


















