3. 错误码设计原则:HTTP风格错误码、业务错误码、错误码分段规则
在构建可观测、可诊断的Go服务时,错误码是客户端与服务器之间、服务与服务之间沟通的“通用语言”。一套设计良好的错误码体系,能显著降低排障成本,提升API的易用性。本章将深入剖析三种核心设计模式:HTTP风格错误码、业务错误码以及错误码分段规则,并给出Go语言中的落地实践。
3.1 错误码设计的两大流派
业界主流错误码设计可分为两大流派:HTTP语义复用型与业务独立编码型。下表对比了它们的核心差异:
| 维度 | HTTP风格错误码 | 业务错误码 |
|---|---|---|
| 编码基础 | 直接复用HTTP Status Code语义 | 自定义数字或字符串编码,独立于HTTP |
| 粒度 | 粗粒度(4xx/5xx大类) | 细粒度(精确到具体业务场景) |
| 可读性 | 通用性强,但缺乏业务上下文 | 业务语义明确,但需维护编码表 |
| 适用场景 | 网关层、通用RESTful API | 核心业务逻辑、领域驱动设计 |
| Go断言难度 | 低(直接比较状态码) | 中(需定义错误码类型与断言函数) |
实战建议: 推荐采用“HTTP风格 + 业务错误码”双层结构。HTTP状态码用于网关路由和快速分类,业务错误码嵌入响应体中用于精确排障。
3.2 HTTP风格错误码:复用标准语义
HTTP风格错误码的核心思想是:用HTTP状态码作为错误的大类标识。例如:
400 Bad Request— 请求参数校验失败401 Unauthorized— 身份认证失败403 Forbidden— 权限不足404 Not Found— 资源不存在409 Conflict— 资源冲突(如重复创建)429 Too Many Requests— 限流触发500 Internal Server Error— 服务端内部错误503 Service Unavailable— 服务熔断或降级
在Go中,通常将HTTP状态码与自定义错误结构体绑定:
// errcode/httperror.go
package errcode
import "net/http"
// HTTPError 携带HTTP状态码的错误
type HTTPError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (e *HTTPError) Error() string {
return e.Message
}
// NewBadRequest 创建400错误
func NewBadRequest(msg string) *HTTPError {
return &HTTPError{Code: http.StatusBadRequest, Message: msg}
}
// NewNotFound 创建404错误
func NewNotFound(msg string) *HTTPError {
return &HTTPError{Code: http.StatusNotFound, Message: msg}
}
避坑提示: 不要将HTTP 200用于业务失败。即使业务逻辑返回“操作失败”,HTTP状态码也应使用4xx或5xx,否则客户端无法通过状态码快速判断请求是否成功。
3.3 业务错误码:精确到原子操作
业务错误码用于在HTTP状态码之下,提供更精细的错误定位。通常采用分段数字编码,例如:
| 错误码范围 | 业务模块 | 示例 |
|---|---|---|
| 10001 ~ 19999 | 用户模块 | 10001: 用户不存在;10002: 密码错误 |
| 20001 ~ 29999 | 订单模块 | 20001: 订单已取消;20002: 库存不足 |
| 30001 ~ 39999 | 支付模块 | 30001: 余额不足;30002: 支付超时 |
| 40001 ~ 49999 | 商品模块 | 40001: 商品已下架;40002: SKU无效 |
Go中定义业务错误码的推荐方式:
// errcode/bizcode.go
package errcode
// BizError 业务错误码
type BizError struct {
BizCode int `json:"biz_code"`
HttpCode int `json:"http_code"` // 关联的HTTP状态码
Message string `json:"message"`
}
func (e *BizError) Error() string {
return e.Message
}
// 预定义业务错误码常量
var (
ErrUserNotFound = &BizError{BizCode: 10001, HttpCode: 404, Message: "用户不存在"}
ErrPasswordWrong = &BizError{BizCode: 10002, HttpCode: 401, Message: "密码错误"}
ErrOrderCancelled = &BizError{BizCode: 20001, HttpCode: 409, Message: "订单已取消"}
ErrInsufficientStock = &BizError{BizCode: 20002, HttpCode: 409, Message: "库存不足"}
)
3.4 错误码分段规则:三段式编码体系
为了在大型项目中保持错误码的可扩展性和可读性,推荐采用三段式分段规则:
- 第一段:服务/模块标识(2位) — 区分微服务或业务域
- 第二段:错误大类(2位) — 区分参数错误、鉴权错误、状态错误、系统错误
- 第三段:具体错误(4位) — 精确到原子操作
例如错误码 01 04 0001:
01= 用户服务04= 鉴权类错误0001= Token过期
下面这张SVG图展示了三段式错误码的完整结构:
3.5 错误码断言实战:从HTTP到业务的完整链路
在Go测试中,断言错误码需要同时验证HTTP状态码和业务错误码。下面是一个完整的断言函数示例:
// assert/error_assert.go
package assert
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"your-project/errcode"
)
// AssertBizError 断言响应中包含指定的业务错误码
func AssertBizError(t *testing.T, resp *httptest.ResponseRecorder, expectedBizCode int) {
t.Helper()
// 1. 断言HTTP状态码为4xx或5xx(非2xx)
if resp.Code < 400 || resp.Code > 599 {
t.Errorf("期望HTTP状态码为4xx/5xx,实际得到 %d", resp.Code)
}
// 2. 解析响应体中的业务错误码
var body struct {
BizCode int `json:"biz_code"`
Message string `json:"message"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
t.Fatalf("解析响应体失败: %v", err)
}
// 3. 断言业务错误码
if body.BizCode != expectedBizCode {
t.Errorf("期望业务错误码 %d,实际得到 %d", expectedBizCode, body.BizCode)
}
}
// 使用示例
func TestCreateUser_DuplicateEmail(t *testing.T) {
// ... 发起请求 ...
// AssertBizError(t, resp, 10003) // 10003 = 邮箱重复
}
设计要点:
- HTTP状态码用于快速判断请求成败,业务错误码用于精确诊断。
- 错误码分段规则应写入团队Wiki,并作为代码评审的必检项。
- Go中建议将错误码定义为不可变常量,避免运行时篡改。
- 断言函数应同时覆盖HTTP状态码和业务错误码,形成双层校验。
3.6 错误码分段与断言的关系
分段规则直接影响断言策略的复杂度。下表总结了不同分段粒度下的断言方式:
| 分段粒度 | 断言策略 | 适用阶段 |
|---|---|---|
| 仅HTTP状态码 | 直接比较 resp.Code |
冒烟测试、健康检查 |
| 模块+大类 | 按位掩码提取模块ID后断言 | 集成测试、模块间契约测试 |
| 完整8位业务码 | 精确匹配 biz_code 字段 |
单元测试、回归测试 |
下面这张SVG图展示了从HTTP状态码到业务错误码的完整断言链路:
3.7 最佳实践总结
- 统一出口: 所有错误码通过统一的错误码中心模块(如
errcode包)导出,禁止在业务代码中硬编码数字。 - 文档即代码: 错误码定义处必须附带注释说明触发条件和修复建议,便于生成API文档。
- 预留号段: 分段规则中预留20%的号段用于未来扩展,避免重构时大规模变更错误码。
- 断言覆盖: 每个业务错误码至少有一个测试用例断言其返回,确保错误码与业务逻辑绑定正确。