直接用c.error()配合末尾统一错误中间件更可靠,需定义含httpstatus、code、msg字段的apperror结构体并实现error()方法,shouldbind失败时调用c.error()而非abortwitherror(),中间件须置于use链末尾且只处理c.errors.last(),同时禁用默认recovery并手写panicrecovery中间件将panic转为apperror。

直接用 c.Error() + 末尾中间件统一处理,比在每个 handler 里手动构造 JSON 更可靠,也避免状态码错乱。
定义带 HTTP 状态码和业务码的自定义 Error 类型
Go 的 error 接口只要实现 Error() 方法就行,但 Gin 的错误链需要额外字段支撑统一响应。你得让 error 自己携带 HTTPStatus 和 Code(业务状态码),而不是靠字符串匹配或 switch 判断。
- 不要用
errors.New("invalid param")这种纯字符串 error,它无法区分 400 还是 500 - 推荐结构体字段:
HTTPStatus int(用于c.AbortWithStatusJSON())、Code int(前端展示的业务码)、Msg string(用户提示) - 必须实现
Error() string,否则c.Errors无法记录;建议返回fmt.Sprintf("[%d] %s", e.Code, e.Msg) - 示例:
type AppError struct { HTTPStatus int Code int Msg string } func (e AppError) Error() string { return fmt.Sprintf("[%d] %s", e.Code, e.Msg) }
ShouldBind 失败时必须用 c.Error() 而非 c.AbortWithError()
c.ShouldBindJSON() 出错时,Gin 默认会把 validator 错误包装成 validator.ValidationErrors,这类 error 不带 HTTP 状态码。如果你直接 c.AbortWithError(http.StatusBadRequest, err),响应体是原始字符串,不走你的 JSON 模板,而且会跳过后续中间件——包括你写的统一错误中间件。
- 正确做法:调用
c.Error(err),仅追加到c.Errors,不中断执行 - 必须紧跟
c.ShouldBindJSON()后判断:if err := c.ShouldBindJSON(&req); err != nil { c.Error(AppError{HTTPStatus: http.StatusBadRequest, Code: 10001, Msg: "参数校验失败"}) return } - 注意:这里
return是为了跳出当前 handler,不是靠c.Abort()终止中间件链;统一格式由末尾中间件兜底
统一错误中间件必须放在 Use 链最后且只取 c.Errors.Last()
Gin 的 c.Errors 是个 slice,按顺序 append,但中间件、日志、recover 等都可能往里写。如果你不取最后一个,容易拿到被前置中间件塞进去的无关 error(比如 auth 中间件记的日志级 warning)。
- 中间件注册顺序必须是:
router.Use(AuthMiddleware(), LoggingMiddleware(), UnifiedErrorMiddleware()),UnifiedErrorMiddleware在最后 - 中间件内必须先
c.Next(),再检查c.Errors,否则拿不到 handler 抛出的 error - 只取
c.Errors.Last(),并确保它是个AppError类型:if len(c.Errors) > 0 { lastErr := c.Errors.Last() if appErr, ok := lastErr.Err.(AppError); ok { c.AbortWithStatusJSON(appErr.HTTPStatus, gin.H{ "code": appErr.Code, "message": appErr.Msg, "data": nil, }) } } - 如果没做类型断言就直接用
lastErr.Err.Error(),会丢失HTTPStatus,只能硬编码 500
panic 场景必须禁用默认 Recovery 并手写中间件
Gin 自带的 gin.Recovery() 会吞掉 panic,只返回固定字符串 {"error":"Internal Server Error"},既不调用 c.Error(),也不进你的统一中间件,还屏蔽堆栈——开发时根本没法调试。
- 启动时显式禁用:
router.Use(gin.RecoveryWithWriter(nil))或更干脆地不调用任何 Recovery - 自己写
PanicRecovery(),用defer+recover(),然后调用c.Error(AppError{...})让它进错误链 - 这个中间件必须放在
Use链最前面,否则后面中间件(如日志)可能已写 header,再 abort 就 panic - 记得用
debug.Stack()或runtime/debug.Stack()捕获堆栈,别只打err.Error()
真正难的不是写结构体,而是所有中间件和 handler 对 c.Error() 和 c.AbortWithStatusJSON() 的语义理解是否一致——前者只记日志,后者才真正响应回去。顺序、类型断言、panic 拦截点,三者错一个,统一格式就漏掉。











