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)。
分层优势:
- 可读性:通过数字分段即可定位问题模块。
- 可扩展:新增错误只需在对应层级追加,不影响已有编码。
- 可过滤:监控系统可按“服务域”或“错误类型”聚合告警。
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 响应只暴露预定义的业务消息。
3.4 设计检查清单
在项目中落地错误码设计时,建议对照以下清单进行评审:
| 检查项 | 通过标准 | 反例 |
|---|---|---|
| 错误码是否分层 | 能从错误码中识别模块和错误类型 | 1001(无上下文) |
| 错误码是否唯一且稳定 | 发布后不修改、不删除,废弃时标记 deprecated | 同一错误在不同版本中含义不同 |
| 错误消息是否可替换 | 消息文案不硬编码在业务逻辑中 | return errors.New("用户不存在") |
| 是否区分内部错误与外部错误 | 内部错误记录日志,外部返回通用消息 | 将 SQL 错误直接返回给客户端 |
| 是否支持国际化 | 消息映射支持多语言切换 | 仅支持中文硬编码 |
实战建议:在项目初期就定义好错误码枚举文件(如
code.go),并编写单元测试验证错误码不重复、消息映射完整。使用 go generate 自动生成错误码文档,保持代码与文档一致。