1. 错误码设计哲学
在 Go 语言中,错误码不是简单的数字枚举,而是一种契约。它定义了函数调用者与被调用者之间的语义边界。良好的错误码设计能显著提升代码的可读性、可调试性与可维护性。本章从三个核心维度展开:错误码的作用、常见设计模式、以及与 HTTP 状态码的映射关系。
1.1 错误码的核心作用
| 作用维度 | 说明 | Go 中的体现 |
|---|---|---|
| 语义化 | 将底层错误(如 io.EOF)转化为业务可理解的错误信息 | 自定义 error 类型携带业务字段 |
| 可断言 | 调用方可以通过 == 或类型断言精确判断错误类型 | 哨兵错误、errors.Is / errors.As |
| 可追溯 | 保留错误链,便于日志与监控定位根因 | fmt.Errorf + %w 包装 |
| 可映射 | 在 API 网关层统一转换为 HTTP 状态码或 gRPC 状态码 | 错误码 → HTTP 状态码映射表 |
设计原则:错误码应遵循“三不”原则——不暴露内部实现细节、不依赖字符串匹配、不将错误码硬编码在业务逻辑中。
1.2 常见设计模式
模式一:哨兵错误(Sentinel Error)
哨兵错误是 Go 中最基础的模式,使用 var 声明包级别的错误变量,调用方通过 == 或 errors.Is 进行断言。
// 定义哨兵错误
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized access")
ErrConflict = errors.New("resource conflict")
)
// 使用
if errors.Is(err, ErrNotFound) {
// 执行 404 处理逻辑
}
适用场景:简单、无额外上下文的错误;标准库与基础组件(如 io.EOF、sql.ErrNoRows)。
模式二:自定义错误类型
当错误需要携带结构化信息(如错误码、HTTP 状态码、内部原因)时,应定义自定义错误类型。
// 自定义错误类型
type AppError struct {
Code int // 业务错误码,如 10001
Message string // 用户可见消息
HTTPStatus int // 映射的 HTTP 状态码
Err error // 内部包装的原始错误
}
func (e *AppError) Error() string {
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Err
}
// 使用 errors.As 断言
var appErr *AppError
if errors.As(err, &appErr) {
// 直接访问 appErr.Code, appErr.HTTPStatus
}
| 特性 | 哨兵错误 | 自定义错误类型 |
|---|---|---|
| 断言方式 | errors.Is |
errors.As |
| 携带上下文 | 否(仅字符串) | 是(结构化字段) |
| 错误链支持 | 需手动包装 | 原生支持 Unwrap |
| 适用层级 | 底层库、基础组件 | 业务层、API 层 |
1.3 错误码与 HTTP 状态码的映射关系
在构建 RESTful API 时,业务错误码需要与 HTTP 状态码建立清晰的映射。以下是一份通用的映射表:
| 业务错误码范围 | HTTP 状态码 | 语义 | 示例场景 |
|---|---|---|---|
| 10001 – 10099 | 400 Bad Request | 请求参数校验失败 | 缺少必填字段、格式错误 |
| 10100 – 10199 | 401 Unauthorized | 身份认证失败 | Token 过期、签名无效 |
| 10200 – 10299 | 403 Forbidden | 权限不足 | 角色无操作权限 |
| 10300 – 10399 | 404 Not Found | 资源不存在 | 用户 ID 不存在 |
| 10400 – 10499 | 409 Conflict | 资源冲突 | 用户名已存在、版本冲突 |
| 10500 – 10599 | 429 Too Many Requests | 频率限制 | API 调用超限 |
| 50001 – 50099 | 500 Internal Server Error | 服务端内部错误 | 数据库连接失败、panic |
避坑提示:不要将 HTTP 状态码直接作为业务错误码使用。例如,不要用
404 作为业务错误码,而应使用 10301 并映射到 HTTP 404。这样可以在不改变 HTTP 语义的前提下,保留业务细粒度。
1.4 错误码设计流程
下图展示了从错误产生到 HTTP 响应的完整链路:
1.5 实战建议
- 统一错误码注册中心:在项目根目录维护一个
errors/codes.go文件,集中声明所有业务错误码与映射关系。 - 避免字符串匹配:永远不要用
strings.Contains(err.Error(), "not found")做断言,应使用errors.Is或errors.As。 - 错误码版本化:一旦发布,错误码不可删除或修改语义,只能废弃(deprecated)并新增。
- 测试覆盖:为每个错误码编写单元测试,验证映射关系与错误链的正确性。
核心结论:错误码设计的本质是契约先行。在写第一行业务代码之前,先定义好错误码体系与映射表,后续的权限验证、API 响应、日志监控都将受益于这一层抽象。