错误码与统一返回体设计

一句话结论:统一返回体解决“客户端怎么解析结果”,建议 {code, message, data, requestId}:code=0(或 200)表示成功,非 0 表示业务失败;错误码集中在一个 errcode 包内定义并文档化,按段位划分(系统/参数/认证授权/业务),只增不改、语义稳定;message 给客户端可读文案,内部细节(堆栈、SQL、第三方响应)只进日志不外泄。实现上用自定义 Error 携带 code 与 HTTP 状态,Gin 侧用响应封装 + 错误收口,避免每个 handler 手写失败分支,并用 requestId 串联日志排障。

返回体与错误码段位

段位含义示例
0成功code=0
1xxxx系统错误10001 内部错误、10002 超时、10003 限流
2xxxx参数/校验20001 参数缺失、20002 格式错误
3xxxx认证授权30001 未登录、30002 token 过期、30003 无权限
4xxxx业务冲突40001 库存不足、40002 重复提交、40003 状态不允许

约定:错误码全局唯一、文档化、一旦发布只增不改;message 可 i18n,detail 可选携带可安全暴露的上下文。

实现骨架

type Error struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    HTTP    int    `json:"-"` // 对应 HTTP 状态
}

var ErrNotFound = &Error{Code: 30003, Message: "无权限", HTTP: 403}

func (e *Error) Error() string { return e.Message }
func OK(c *gin.Context, data any) {
    c.JSON(200, gin.H{"code": 0, "message": "ok", "data": data,
        "requestId": c.GetString("request_id")})
}

func Fail(c *gin.Context, err error) {
    var e *Error
    if errors.As(err, &e) {
        c.JSON(e.HTTP, gin.H{"code": e.Code, "message": e.Message,
            "requestId": c.GetString("request_id")})
        return
    }
    log.Error("未归类错误", "err", err, "requestId", ...) // 内部细节只进日志
    c.JSON(500, gin.H{"code": 10001, "message": "系统繁忙", ...})
}

handler 只做一件事:if err := svc.Do(...); err != nil { Fail(c, err); return }

设计要点

  • 不要 20 个 handler 各写一套 JSON 失败分支,成功/失败都走统一封装。
  • 业务失败到底用 200+code 还是 HTTP 语义状态码?对外 API 建议 HTTP 状态码(4xx/5xx)表达大类 + body code 表达细节;内部老系统可全 200+code,全团队一致即可。
  • panic 兜底:gin.Recovery 或自定义 Recovery 把 panic 转 500 并打堆栈日志,别让请求裸 500 无日志。
  • requestId 由入口中间件生成并注入日志(与日志体系章节配合),出问题能按 id 拉全链路日志。

追问记忆点

  • 追问:错误码为什么要文档化且只增不改?——客户端/网关按 code 分支,改语义等于线上事故;新增码要进文档与测试。
  • 追问:handler 层错误和 service 层错误怎么衔接?——service 返回带 code 的错误(或 errors.Join 包装),handler 用 errors.As 还原成响应。
  • 追问:10001 和 500 都表示内部错误,重复吗?——HTTP 500 面向传输层,code 面向业务分支,二者配合而非二选一。
  • 记忆点:统一 {code,message,data,requestId};错误码分段、稳定、集中定义;细节进日志不进响应;收口错误与 panic。
笔记加载中…