必须定义统一错误响应结构体errresponse,分离业务码code与http状态码httpstatus,实现error接口,预定义常见错误,全局recover拦截并透传错误至外层统一响应。

直接用 c.JSON 返回错误对象,但不统一结构、不区分业务码和 HTTP 状态码,会导致前端解析混乱、日志难排查、监控指标失真。必须定义一个可复用、可扩展、与 HTTP 语义对齐的错误响应结构。
定义统一错误响应结构体
不要在每个 handler 里手写 gin.H{"code": 1001, "msg": "xxx", "data": nil} —— 这无法类型检查,易拼错字段,也不支持嵌套错误或 traceID。应该定义一个结构体,并导出为公共类型:
-
Code字段表示业务错误码(如1001表示用户不存在),和 HTTP 状态码分离 -
HTTPStatus字段显式控制实际返回的 HTTP 状态码(如404或400),避免误用c.AbortWithStatus(200) -
Msg仅用于前端展示,不带敏感信息;DebugMsg(可选)用于日志,仅在 debug 模式下透出 - 实现
error接口,方便在中间件中用errors.Is()判断类型
示例:
type ErrResponse struct {
Code int `json:"code"`
HTTPStatus int `json:"-"` // 不序列化到 JSON
Msg string `json:"msg"`
Data any `json:"data,omitempty"`
}
func (e *ErrResponse) Error() string { return e.Msg }
func NewErr(code, httpStatus int, msg string) *ErrResponse {
return &ErrResponse{Code: code, HTTPStatus: httpStatus, Msg: msg}
}
在 handler 中正确返回错误
别再用 c.AbortWithStatus(http.StatusBadRequest) + 单独 c.JSON 两步走 —— 容易漏掉状态码或写错顺序,且无法保证结构一致。
- 封装一个
c.Error(err)方法(通过中间件注入或扩展*gin.Context) - 或者更推荐:直接调用
c.JSON(err.HTTPStatus, err),前提是确保err.HTTPStatus已设好 - 对校验失败等常见场景,预定义错误变量,如
ErrParamInvalid = NewErr(2001, http.StatusBadRequest, "参数格式错误") - 不要把数据库错误(如
sql.ErrNoRows)直接返回给前端,需转换为业务错误
全局错误拦截与 recover 处理
panic 必须被捕获,但 recover() 后不能简单返回 500 —— 缺少上下文、无法分类、不利于定位。
- 在 recover 中判断 panic 值类型:如果是自定义错误(如
*ErrResponse),直接取其HTTPStatus和结构 - 如果是未预期 panic(如空指针),统一返回
500,但带上 traceID 和时间戳,便于日志关联 - 避免在 recover 中调用
c.Abort()后又执行后续 handler —— Gin 的Abort()是单向终止,但 recover 本身已打断流程,重复 Abort 可能引发 panic - 注意:Gin 默认中间件(如
recovery)会打印堆栈,生产环境应关闭或重定向到日志系统
与中间件链配合控制错误传播
错误不是“抛出去就完事”,而是要沿中间件链向上透传,直到被最终响应逻辑捕获。
- 不要在中间件里用
c.Abort()就结束,除非你确定后续所有 handler 都不该执行 - 推荐方式:中间件检测到异常时,调用
c.Set("err", err),然后c.Next()继续,由最外层统一响应 - 若使用
c.Error(err)扩展方法,确保它内部调用c.Abort()并设置状态,否则可能重复响应 - 特别注意:
c.Render()和c.JSON()都会自动调用c.Writer.WriteHeader(),一旦写入 header,再调用c.AbortWithStatus()会 panic
真正麻烦的不是定义结构,而是让所有人——包括新来的同事——在 every handler、every middleware、every recover 分支里都用同一套规则。结构可以复制粘贴,一致性得靠约束和工具(比如 linter 检查是否用了裸 gin.H)。











