3. 错误码设计原则:HTTP 状态码类比、业务错误码分层、错误码与错误消息分离

在构建可维护的 Go 服务时,错误码设计是 API 契约的核心。混乱的错误码会导致客户端解析困难、排查效率低下,甚至引发安全漏洞。本章借鉴 HTTP 状态码的分层思想,结合业务场景,提炼出三条核心设计原则:HTTP 状态码类比、业务错误码分层、错误码与错误消息分离。

3.1 HTTP 状态码类比:从网络协议到业务语义

HTTP 状态码通过数字范围表达请求结果(2xx 成功、4xx 客户端错误、5xx 服务端错误)。业务错误码可以借鉴这一思想,将错误按“域”划分,使开发者仅凭错误码前缀就能判断错误来源。

HTTP 状态码范围 语义 业务错误码类比 示例
2xx 成功 0 / 200000 成功响应
4xx 客户端错误 400xxx 参数校验失败、权限不足
5xx 服务端错误 500xxx 数据库异常、内部超时
设计建议:业务错误码的首位数字直接映射 HTTP 状态码大类。例如 400101 表示“请求参数缺失”,403001 表示“无操作权限”。这种映射让网关层和客户端可以快速做统一拦截。

3.2 业务错误码分层:模块化与可扩展

单一的数字列表无法应对复杂业务。推荐采用 分层编码 结构,将错误码拆解为多个字段,每个字段代表一个维度。常见的分层模型为:

  • 服务域(2 位):标识微服务或业务模块,如 01 用户服务、02 订单服务。
  • 错误类型(2 位):标识错误大类,如 01 参数错误、02 鉴权错误、03 资源不存在。
  • 具体错误(2 位):标识具体的错误原因,如 01 邮箱格式错误、02 密码长度不足。

例如错误码 010201 可解读为:用户服务(01)→ 鉴权错误(02)→ 令牌过期(01)。

分层优势:
  • 可读性:通过数字分段即可定位问题模块。
  • 可扩展:新增错误只需在对应层级追加,不影响已有编码。
  • 可过滤:监控系统可按“服务域”或“错误类型”聚合告警。
业务错误码分层结构 业务错误码分层结构(6 位数字编码) 服务域 01 用户服务 02 订单服务 错误类型 01 参数错误 02 鉴权错误 具体错误 01 令牌过期 02 签名无效 示例:错误码 010201 → 用户服务 · 鉴权错误 · 令牌过期 分层编码让错误码具备自解释能力,无需查表即可定位问题域

3.3 错误码与错误消息分离:面向机器与面向人类

错误码是给程序读的,错误消息是给人读的。两者必须分离,原因如下:

  • 国际化:同一错误码在不同语言环境下展示不同消息。
  • 安全性:错误消息可能包含敏感信息(如数据库表名、堆栈路径),不应直接暴露给客户端。
  • 灵活性:后端可以独立更新错误消息文案,无需修改错误码定义。

在 Go 中,推荐使用结构体将错误码与消息绑定,并通过接口对外暴露:

// Code 定义业务错误码
type Code int

// Err 携带错误码与消息的结构体
type Err struct {
    Code    Code   `json:"code"`
    Message string `json:"message"`
    // 内部可附加详细错误,但不序列化
    internalErr error
}

// 预定义错误码
const (
    ErrSuccess       Code = 0
    ErrParamInvalid  Code = 400101
    ErrTokenExpired  Code = 401001
    ErrPermissionDeny Code = 403001
)

// 错误消息映射(可替换为 i18n 实现)
var codeMessages = map[Code]string{
    ErrSuccess:       "success",
    ErrParamInvalid:  "请求参数无效",
    ErrTokenExpired:  "令牌已过期",
    ErrPermissionDeny: "无操作权限",
}

// NewErr 创建对外错误
func NewErr(code Code) *Err {
    msg, ok := codeMessages[code]
    if !ok {
        msg = "未知错误"
    }
    return &Err{Code: code, Message: msg}
}

// Error 实现 error 接口
func (e *Err) Error() string {
    return fmt.Sprintf("code=%d, message=%s", e.Code, e.Message)
}
常见误区:不要将 internalErr 或堆栈信息拼接到 Message 字段中返回给客户端。错误消息应保持简洁、用户友好。内部日志可以记录完整错误链,但 API 响应只暴露预定义的业务消息。
错误码与错误消息分离流程 错误码与错误消息分离:面向机器 vs 面向人类 错误码(Code) 面向机器 固定数字标识,不可变 错误消息(Message) 面向人类 可国际化、可定制 映射层 code → message 客户端根据 code 做逻辑判断,根据 message 做展示 后端可随时更新 message 文案,不影响客户端解析

3.4 设计检查清单

在项目中落地错误码设计时,建议对照以下清单进行评审:

检查项 通过标准 反例
错误码是否分层 能从错误码中识别模块和错误类型 1001(无上下文)
错误码是否唯一且稳定 发布后不修改、不删除,废弃时标记 deprecated 同一错误在不同版本中含义不同
错误消息是否可替换 消息文案不硬编码在业务逻辑中 return errors.New("用户不存在")
是否区分内部错误与外部错误 内部错误记录日志,外部返回通用消息 将 SQL 错误直接返回给客户端
是否支持国际化 消息映射支持多语言切换 仅支持中文硬编码
实战建议:在项目初期就定义好错误码枚举文件(如 code.go),并编写单元测试验证错误码不重复、消息映射完整。使用 go generate 自动生成错误码文档,保持代码与文档一致。