1. 错误码哲学:为什么需要错误码断言?领域边界与防御式编程的关系
1.1 从“错误即值”到“错误即边界”
Go 语言将错误作为一等公民——error 是一个接口,而非异常。这种设计迫使开发者显式处理每个错误,但也带来了新的挑战:错误在传递过程中会丢失语义。一个来自底层数据库的 io.EOF,经过三层业务封装后,调用方只能看到“发生了错误”,却无法判断是“数据不存在”还是“连接断开”。
错误码断言的核心哲学是:在领域边界处,将错误从“值”转化为“断言”。即,在模块的入口和出口,对错误进行结构化编码,使调用方能够通过错误码精确判断错误的性质、来源和可恢复性。
核心观点: 错误码不是日志,不是字符串,而是领域边界上的契约断言。它告诉调用方:“这个错误属于哪个域?是否可以重试?是否需要告警?”
1.2 领域边界:错误的“海关检查站”
在软件架构中,领域边界是模块、服务、层之间的分界线。每一次跨边界调用,都是错误语义丢失的高风险点。下表展示了不同边界类型下的错误传递风险:
| 边界类型 | 典型场景 | 错误传递风险 | 错误码断言价值 |
|---|---|---|---|
| 基础设施 → 领域 | 数据库查询、HTTP 调用 | 底层错误暴露给业务逻辑 | 将 io.EOF 映射为 ErrDataNotFound |
| 领域 → 应用服务 | 业务规则校验、聚合操作 | 业务错误被包装为通用 error | 通过错误码区分“参数错误”与“状态冲突” |
| 应用服务 → 传输层 | gRPC、HTTP API 响应 | 内部错误泄露到客户端 | 统一错误码格式,隐藏实现细节 |
| 进程间/服务间 | 微服务调用、消息队列 | 序列化/反序列化丢失错误类型 | 错误码作为稳定契约,不受语言类型影响 |
错误码断言正是在这些边界上建立“海关检查站”:只允许结构化的、带错误码的错误通过,而将原始的、无结构的错误拦截在边界内部。
1.3 防御式编程:在边界处“设卡”
防御式编程(Defensive Programming)强调:不要信任任何跨边界传入的数据。错误码断言是防御式编程在错误处理领域的具体实践。它要求开发者在每个领域边界处执行以下三个步骤:
- 断言输入:检查传入参数是否合法,若不合法则返回预定义的错误码。
- 断言调用结果:对下层返回的错误进行断言,将其映射为当前领域的错误码。
- 断言输出:确保返回给上层的错误携带正确的错误码和上下文。
实践建议: 在 Go 中,使用
errors.Is 和 errors.As 进行错误断言,而不是直接比较字符串。错误码应定义为 type ErrCode int 或 type ErrCode string,并实现 error 接口。
1.4 错误码断言 vs. 传统错误处理:对比分析
| 维度 | 传统错误处理 | 错误码断言 |
|---|---|---|
| 错误表示 | error 接口 + 字符串消息 |
结构化错误码 + 可选消息 |
| 跨边界传递 | 逐层包装,语义稀释 | 错误码不变,语义稳定 |
| 调用方判断 | 字符串匹配或类型断言 | errors.Is / errors.As 断言 |
| 可恢复性 | 难以区分可重试与不可重试 | 错误码分类明确(临时/永久) |
| 测试性 | 依赖错误消息,脆弱 | 依赖错误码,稳定 |
| 文档化 | 散落在代码注释中 | 集中定义,可作为 API 契约 |
1.5 领域边界防御架构图
下图展示了在典型的三层架构中,错误码断言如何在每个领域边界发挥作用:
1.6 错误码断言的核心收益
- 契约稳定性:错误码作为 API 契约的一部分,不会因内部实现变更而改变。
- 可观测性:监控系统可以直接基于错误码进行告警和统计,无需解析字符串。
- 可测试性:单元测试中,断言错误码比断言错误消息更可靠、更简洁。
- 可恢复性:调用方可以根据错误码决定是否重试、降级或报错。
- 安全性:避免将内部实现细节(如堆栈信息、数据库表名)泄露到外部。
常见误区: 错误码断言不是“给每个错误起个名字”。它是在领域边界上建立的结构化错误分类体系。滥用错误码(如为每个可能的分支都定义独立错误码)会导致维护成本剧增。建议每个领域模块的错误码数量控制在 10~30 个。