必须禁用默认gin.Recovery(),因其返回HTML而非JSON、无code字段、不透传request_id,与自定义错误中间件共存易触发多次WriteHeader panic;需改用CustomRecovery()配合c.AbortWithStatusJSON()输出结构化JSON,确保错误码、HTTP状态码、traceID、日志全链路对齐。

为什么默认 gin.Recovery() 必须禁用
它返回 HTML 页面,不是 JSON;不带 code 字段,也不透传 request_id;和你写的业务错误中间件共存时,容易触发 http: multiple response.WriteHeader calls panic。更隐蔽的是:它只捕获 panic,对 return errors.New("xxx") 或参数校验失败这类“非崩溃型错误”完全无感——这些错误若没被显式检查,就直接漏出未格式化的响应。
CustomRecovery() 中 defer 里的 return 不能省
否则 c.Next() 会继续执行,可能二次写响应头或 body,导致 panic 或响应错乱。正确做法是:
func CustomRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
log.Printf("PANIC: %+v", err)
debug.PrintStack()
c.AbortWithStatusJSON(http.StatusInternalServerError, response.Error(5000, "系统异常"))
return // ← 这个 return 必须有
}
}()
c.Next()
}
}
- 别在
CustomRecovery()里调用c.Error(),它只是存错信号,不是输出动作 - 日志必须用
log.Printf("PANIC: %+v", err),fmt.Printf会丢堆栈 - 注册顺序必须最前:
r.Use(CustomRecovery()),早于所有其他中间件
业务错误怎么进统一 JSON 流水线
c.Error() 不等于响应,它只是把错误塞进 c.Errors 队列;真正干活的是后续中间件读取并转换。所以你需要一个紧接在 CustomRecovery() 后的兜底中间件:
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
err := c.Errors.Last()
switch {
case errors.Is(err.Err, ErrInvalidParam):
c.AbortWithStatusJSON(http.StatusBadRequest, response.Error(1001, "参数错误"))
case errors.Is(err.Err, ErrUserNotFound):
c.AbortWithStatusJSON(http.StatusNotFound, response.Error(4001, "用户不存在"))
default:
c.AbortWithStatusJSON(http.StatusInternalServerError, response.Error(5000, "系统异常"))
}
}
}
}
- 检查必须用
len(c.Errors) > 0,不能靠err != nil判断 - 错误类型建议实现
interface{ Status() int },方便提取 HTTP 状态码 - 禁止用
err.Error() == "xxx"字符串匹配,要用errors.Is()或自定义 error 类型 - 别在多个中间件里重复调用
c.AbortWithStatusJSON(),否则 panic
错误码和响应结构怎么避免硬编码
业务错误码不能散落在 handler 里写 1001、"参数错误",必须集中定义、可导出、带语义。推荐结构:
// pkg/errcode/errcode.go
const (
UserNotFound = 4001
InvalidParam = 1001
)
func Text(code int) string {
switch code {
case UserNotFound:
return "用户不存在"
case InvalidParam:
return "参数错误"
}
return "未知错误"
}
func NewBadRequest(code int) *AppError {
return &AppError{
Code: code,
Msg: Text(code),
Status: http.StatusBadRequest,
}
}
- 响应结构体必须同时含
code(业务码)和status(HTTP 状态码),例如{"code":1001,"message":"参数校验失败","status":400,"request_id":"abc123"} - 前端解析只依赖
code和message;网关、监控系统看status;request_id是链路追踪刚需 - 敏感信息(如 SQL 片段、文件路径)绝不能直接塞进
message,要抽象为用户友好的提示


















