3. 错误码设计原则:HTTP 状态码类比、业务错误码分类、错误码的层级结构
在构建搜索服务的断言接口契约时,错误码是客户端与服务器之间最直接的沟通语言。一套清晰、可扩展的错误码体系,能显著降低前后端联调成本,提升系统可观测性。本章从 HTTP 状态码的语义映射出发,逐步深入到业务错误码的分类与层级结构设计。
3.1 HTTP 状态码类比:从传输层到业务层
HTTP 状态码定义了请求的传输结果(如 200 成功、404 未找到、500 服务器错误)。但在微服务架构中,仅靠 HTTP 状态码无法表达业务层面的具体失败原因。因此,我们需要在 HTTP 状态码之上构建一套业务错误码体系,形成“传输层 + 业务层”的双层语义。
| HTTP 状态码 | 语义 | 业务错误码映射原则 |
|---|---|---|
| 2xx | 成功 | 业务正常返回,错误码为 0 或空 |
| 4xx | 客户端错误 | 参数校验失败、资源不存在、权限不足等 |
| 5xx | 服务端错误 | 内部异常、依赖超时、数据库故障等 |
设计原则:业务错误码不应直接复用 HTTP 状态码的数字,而应使用独立的编码体系(如 10001、20003),避免与传输层状态码混淆。
3.2 业务错误码分类:四大核心类别
根据搜索服务的典型场景,我们将业务错误码划分为以下四大类别:
- 参数错误(P类):请求参数格式、类型、范围不合法。例如:缺少必填字段、排序字段不存在、分页参数超出上限。
- 资源错误(R类):请求的资源不存在、已删除或状态异常。例如:索引不存在、文档 ID 无效、搜索模板未发布。
- 权限错误(A类):认证失败或授权不足。例如:API Key 无效、IP 白名单未通过、操作超出角色权限。
- 系统错误(S类):服务内部故障或依赖异常。例如:Elasticsearch 连接超时、内存溢出、配置加载失败。
编码示例:
P-1001: 参数缺失
R-2003: 索引不存在
A-3002: API Key 已过期
S-4005: 搜索后端超时
P-1001: 参数缺失
R-2003: 索引不存在
A-3002: API Key 已过期
S-4005: 搜索后端超时
3.3 错误码的层级结构:三段式编码
为了支持大规模系统的错误码管理,我们推荐采用 三段式层级结构:
错误码 = 服务域(2位) + 错误类别(2位) + 具体错误(4位)
示例:01-P-1001
├── 01: 搜索服务域
├── P: 参数错误类别
└── 1001: 具体错误编号
这种结构具备以下优势:
- 可扩展:每个段独立增长,不会因新增错误而打乱整体编号。
- 可过滤:监控系统可按服务域或错误类别快速聚合告警。
- 可读性强:开发人员从错误码前四位即可定位问题范围。
3.4 错误码断言接口契约
在 Go 搜索服务的 API 响应中,错误码应作为结构化字段返回,而非仅依赖 HTTP 状态码。以下是一个标准的错误响应契约:
{
"code": "01-P-1001",
"message": "缺少必填字段: query",
"details": {
"field": "query",
"reason": "required"
},
"trace_id": "abc-123-def"
}
常见陷阱:不要在错误消息中直接拼接用户输入,防止注入攻击;不要将内部堆栈信息暴露给客户端。
3.5 错误码与 HTTP 状态码的映射矩阵
在实际网关层,需要将业务错误码映射回合适的 HTTP 状态码,以便客户端框架(如浏览器、移动端 SDK)能正确触发重试或错误处理逻辑。
| 业务错误码类别 | 示例错误码 | 映射 HTTP 状态码 | 说明 |
|---|---|---|---|
| 参数错误 (P) | 01-P-1001 | 400 Bad Request | 客户端请求格式错误 |
| 资源错误 (R) | 01-R-2003 | 404 Not Found | 请求的资源不存在 |
| 权限错误 (A) | 01-A-3002 | 401 Unauthorized | 认证失败或凭证过期 |
| 系统错误 (S) | 01-S-4005 | 503 Service Unavailable | 后端依赖不可用 |
3.6 错误码管理最佳实践
- 集中注册:所有错误码在项目初期统一注册到
errors.go或独立的错误码配置文件,避免散落在业务代码中。 - 版本化:错误码一旦发布,不应修改其语义;如需废弃,标记为 deprecated 并保留至少一个大版本周期。
- 文档化:每个错误码附带中文描述、触发条件、建议处理方式,生成 API 文档时自动包含。
- 可测试:编写单元测试断言每个错误码的生成路径,确保重构时不会遗漏或误改。
Go 代码示例:错误码定义与断言
// errors/codes.go
const (
ErrMissingField = "01-P-1001"
ErrIndexNotFound = "01-R-2003"
ErrAPIKeyExpired = "01-A-3002"
ErrBackendTimeout = "01-S-4005"
)
// 测试断言示例
func TestErrorCode(t *testing.T) {
resp := searchAPI.Search(context.Background(), &SearchRequest{})
assert.Equal(t, "01-P-1001", resp.Error.Code)
assert.Contains(t, resp.Error.Message, "query")
}