1、错误码设计哲学

1.1 Go 语言中错误处理的哲学

Go 语言在错误处理上遵循 显式、简洁、可组合 的哲学。与许多语言通过异常(Exception)将错误抛到调用栈上层不同,Go 要求函数调用者显式检查错误返回值。这种设计迫使开发者将错误视为函数契约的一部分,而不是意外事件。

Go 错误处理的核心原则包括:

  • 错误是值(Errors are values):错误与普通值一样,可以被赋值、传递、比较和组合。
  • 尽早检查(Check early):在函数返回后立即检查错误,避免错误状态扩散。
  • 不忽略错误(Don't ignore errors):使用 _ 忽略错误返回值是代码异味,应尽量避免。
  • 错误携带上下文(Wrap errors with context):通过 fmt.Errorf("...: %w", err) 或第三方库为错误添加调用栈和业务信息。
Go 错误处理格言: "Errors are just values. They are not exceptions." — Rob Pike

1.2 错误码与异常的区别

错误码(Error Code)和异常(Exception)是两种截然不同的错误传播机制。下表从多个维度对比两者的差异:

对比维度 错误码(Go 风格) 异常(Java/Python 风格)
传播方式 显式返回值,调用者必须检查 隐式栈展开,自动向上抛出
控制流 正常返回路径,if 分支处理 异常跳转,破坏正常控制流
性能开销 几乎为零(值拷贝) 较高(栈展开、对象构造)
类型安全 编译时检查(error 接口) 运行时检查(catch 类型匹配)
可组合性 强(error 可包装、可断言) 弱(异常链依赖语言特性)
适用场景 预期错误(文件不存在、网络超时) 非预期错误(空指针、数组越界)
实践建议: Go 中不应使用 panic/recover 模拟异常。panic 仅用于程序无法继续运行的致命错误(如初始化失败、死锁),而非业务逻辑中的预期错误。

1.3 为什么需要安全错误码

在微服务、API 网关、分布式系统中,原始 Go error 接口存在以下安全与可维护性问题:

  1. 信息泄露风险:原始错误字符串可能包含文件路径、SQL 语句、堆栈信息等敏感内容,直接返回给客户端会暴露系统内部细节。
  2. 契约不明确:调用者无法从函数签名中获知可能返回哪些错误,只能通过文档或运行时猜测。
  3. 难以国际化:错误消息硬编码为英文,无法根据客户端语言环境切换。
  4. 调试困难:生产环境中无法通过错误码快速定位问题模块和根因。

安全错误码 通过将错误抽象为 结构化、可枚举、带语义 的代码,解决了上述问题。其核心价值包括:

  • 安全隔离:对外只暴露错误码和通用消息,隐藏内部实现细节。
  • 契约断言:通过接口定义明确的错误码集合,调用方可以精确处理每种错误。
  • 可观测性:错误码可直接用于监控告警、日志聚合和故障排查。
  • 多语言支持:错误码作为键值,映射到不同语言的用户友好消息。
错误码 vs 异常:传播机制对比 错误码(Go 风格) 函数调用 返回 (result, error) if err != nil { 处理 } 安全错误码断言 异常(Java/Python 风格) 函数调用 抛出异常(throw) 栈展开(隐式传播) catch 捕获处理 错误码:显式、可组合、编译期安全 | 异常:隐式、破坏控制流、运行时开销

1.4 安全错误码的设计原则

设计一套健壮的安全错误码体系,需要遵循以下原则:

原则 说明 示例
唯一性 每个错误码全局唯一,不重复使用 ErrUserNotFound = 1001
可扩展 按模块/层级分配码段,支持新增 用户模块 1000-1999,订单模块 2000-2999
语义化 错误码名称清晰表达错误含义 ErrInsufficientBalance 而非 Err1003
安全隔离 对外隐藏内部细节,只暴露通用消息 对外:"用户不存在";对内:"user_id=123 not found in db"
可断言 支持通过接口或类型断言精确匹配 errors.Is(err, ErrUserNotFound)
安全警告: 永远不要将原始 error.Error() 字符串直接返回给 HTTP 客户端或写入用户可见的日志。这可能导致敏感信息泄露(如数据库连接字符串、文件路径、内部 IP 地址)。始终使用安全错误码 + 预定义消息模板。
安全错误码分层架构 安全错误码分层架构 对外接口层(API / gRPC / HTTP) 返回:安全错误码 + 通用消息(无内部细节) 业务逻辑层(Service / UseCase) 使用:语义化错误码 + 包装上下文(Wrap) 数据 / 基础设施层(DB / Redis / RPC) 产生:原始 error(含内部细节,不对外暴露) 错误码映射表(Code ↔ Message ↔ i18n) 统一管理:错误码定义、对外消息、日志级别、HTTP 状态码

1.5 安全错误码的典型定义示例

以下是一个 Go 中安全错误码的推荐定义模式:

// errcode/user.go — 用户模块错误码
package errcode

import "errors"

// 用户模块错误码段:1000-1999
var (
    // 通用错误
    ErrInternal     = &Error{Code: 1000, Message: "内部服务错误"}
    ErrBadRequest   = &Error{Code: 1001, Message: "请求参数错误"}

    // 用户认证
    ErrUserNotFound     = &Error{Code: 1101, Message: "用户不存在"}
    ErrUserDisabled     = &Error{Code: 1102, Message: "用户已被禁用"}
    ErrPasswordMismatch = &Error{Code: 1103, Message: "密码错误"}
    ErrTokenExpired     = &Error{Code: 1104, Message: "令牌已过期"}

    // 权限
    ErrPermissionDenied = &Error{Code: 1201, Message: "权限不足"}
)

// Error 结构体实现 error 接口,支持安全断言
type Error struct {
    Code    int    // 安全错误码
    Message string // 对外通用消息(无内部细节)
}

func (e *Error) Error() string {
    return e.Message
}

// 使用示例
func GetUser(id string) (*User, error) {
    user, err := db.FindUser(id)
    if errors.Is(err, db.ErrNotFound) {
        return nil, ErrUserNotFound  // 对外只暴露安全错误码
    }
    if err != nil {
        return nil, ErrInternal      // 内部错误统一包装
    }
    return user, nil
}
关键点: 上述示例中,Error 结构体同时实现了 error 接口和自定义断言能力。调用方可以通过 errors.Is(err, errcode.ErrUserNotFound) 精确匹配错误类型,而无需解析错误字符串。对外返回的 Message 字段不包含任何内部实现细节,满足安全隔离要求。