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 响应的完整链路:

错误码设计流程:从业务错误到 HTTP 响应 业务层 返回 AppError 错误码映射层 Code → HTTP Status API 网关 统一响应格式 日志与监控 记录 Code + 原始错误 客户端 解析 Code 做断言 关键设计点 1. 错误码不可变 2. 映射表集中管理

1.5 实战建议

  • 统一错误码注册中心:在项目根目录维护一个 errors/codes.go 文件,集中声明所有业务错误码与映射关系。
  • 避免字符串匹配:永远不要用 strings.Contains(err.Error(), "not found") 做断言,应使用 errors.Is 或 errors.As。
  • 错误码版本化:一旦发布,错误码不可删除或修改语义,只能废弃(deprecated)并新增。
  • 测试覆盖:为每个错误码编写单元测试,验证映射关系与错误链的正确性。
核心结论:错误码设计的本质是契约先行。在写第一行业务代码之前,先定义好错误码体系与映射表,后续的权限验证、API 响应、日志监控都将受益于这一层抽象。