不能用recover拦截业务错误,因为业务错误(如参数校验失败、数据库查不到记录)属于正常控制流,应通过error返回值显式处理;recover仅用于兜底捕获真正不可恢复的panic(如空指针、切片越界),滥用会掩盖问题、丢失调用栈、破坏错误上下文。

为什么不能用 recover 拦截业务错误
因为 recover 只能捕获 panic,而业务错误(如参数校验失败、数据库查不到记录)不该触发 panic——它们是正常控制流的一部分。用 recover 去“抓” error,本质是把 error 当异常处理,结果只会掩盖真实问题、丢失调用栈、让日志无法追溯源头。
Echo 默认的 Recover 中间件只该用于兜底:防止空指针、切片越界等真正不可恢复的崩溃,而不是替代错误传递逻辑。
- 所有 handler 必须显式返回
error,不要在函数内直接c.JSON(400, ...) - 自定义错误类型(如
AppError)应实现Error()方法,并带Code、StatusCode字段 - 用
fmt.Errorf("xxx: %w", err)包装下游错误,保留原始错误链
如何注册自定义 HTTPErrorHandler
Echo 提供了 e.HTTPErrorHandler 接口,它是统一错误响应的入口,但不是“捕获器”——它只处理你主动返回的 error,包括 echo.HTTPError 和你自己的结构体。
典型写法是在初始化 Echo 实例后立即赋值:
e.HTTPErrorHandler = func(err error, c echo.Context) {
code := http.StatusInternalServerError
message := "Internal Server Error"
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
message = he.Message
}
if appErr, ok := err.(*AppError); ok {
code = appErr.StatusCode
message = appErr.Message // 仅用于日志,不直接返回给前端
}
// 生产环境隐藏细节,只返回通用提示 + code
if code >= 500 {
message = "Something went wrong"
}
c.JSON(code, map[string]interface{}{
"code": code,
"message": message,
"trace_id": c.Request().Context().Value("trace_id"),
})
}
- 别在
HTTPErrorHandler里再调用recover()——它不接收 panic - 如果需要区分错误类型做不同响应(如 401 不带 trace_id),就靠类型断言判断
- 确保
AppError的StatusCode字段与 HTTP 状态码一致,避免中间件二次映射
AppError 结构体该包含哪些字段
一个可落地的 AppError 至少要支持日志归类、监控告警、前端映射三件事,硬编码字符串或只靠 Error() 返回值远远不够。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
推荐字段组合:
type AppError struct {
Code int `json:"code"` // 业务错误码,如 4001(参数校验失败)
StatusCode int `json:"status_code"` // HTTP 状态码,如 400/404/500
Message string `json:"-"` // 开发者调试信息,含参数、上下文 ID,不暴露给前端
Details map[string]interface{} `json:"details,omitempty"` // 具体失败字段、期望值/实际值等
}
-
Message字段必须含上下文(如"user_id=123, req_id=abc"),否则线上排查等于盲人摸象 - 不要把用户提示文案塞进
Message或Error()返回值——前端应根据Code查表渲染 -
Details是可选但极有用:比如参数校验失败时填{"field": "email", "reason": "invalid format"}
中间件中如何透传和终止错误
错误不该在中间件里“吃掉”,而应通过 return err 向上抛,最终由 HTTPErrorHandler 统一收口。中间件唯一合法的终止方式是返回非 nil error。
例如鉴权中间件:
func AuthMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")
if token == "" {
return &AppError{
Code: 4001,
StatusCode: http.StatusUnauthorized,
Message: "missing auth header",
Details: map[string]interface{}{"header": "Authorization"},
}
}
// 验证通过,继续
return next(c)
}
}
}
- 中间件内不要调用
c.JSON或c.String——这会绕过HTTPErrorHandler,导致响应格式不一致 - 若需提前终止并返回错误,必须
return err;若只是记录日志或修改上下文,就return next(c) - 所有中间件都应放在
e.Use()中注册,确保洋葱模型生效;路由级中间件同理,但作用域更小
最易被忽略的一点:错误包装链必须完整。哪怕只在 handler 最外层用一次 fmt.Errorf("failed to get user: %w", err),也比裸传 err 强——否则日志里永远只有最后一层错误,找不到根因。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










