必须区分业务错误和系统错误:业务错误(如“订单不存在”)需转为用户提示,用BusinessError类型和errors.Is判断;系统错误(如数据库失败)需记录日志并降级,用SystemError包裹原始错误;HTTP响应须按类型映射状态码,测试必须覆盖错误路径及哨兵变量断言。

错误类型必须区分业务错误和系统错误
Go 的 error 接口太宽泛,直接用 errors.New 或 fmt.Errorf 会导致调用方无法可靠识别错误性质。业务错误(如“订单不存在”“库存不足”)需要被上层逻辑捕获并转成用户友好的提示;系统错误(如数据库连接失败、网络超时)则应记录日志并触发降级或重试。
推荐定义两个基础错误类型:
type BusinessError struct {
Code string
Message string
Details map[string]interface{}
}
func (e *BusinessError) Error() string { return e.Message }
type SystemError struct {
Err error
Context string
}
func (e *SystemError) Error() string { return fmt.Sprintf("%s: %v", e.Context, e.Err) }
关键点:
-
BusinessError不嵌套底层错误,避免泄露敏感信息;Code字段用于前端映射文案或监控告警 -
SystemError必须包裹原始error,保留堆栈和底层原因,方便排查 - 禁止在
BusinessError.Error()中拼接底层错误字符串
用 errors.Is 和 errors.As 判断错误链而非字符串匹配
很多人写 if strings.Contains(err.Error(), "not found"),这极不可靠:一旦错误消息微调就失效,且无法跨语言本地化。Go 1.13+ 的错误链机制才是正解。
立即学习“go语言免费学习笔记(深入)”;
标准做法是定义可识别的哨兵错误或自定义类型:
var (
ErrOrderNotFound = &BusinessError{Code: "ORDER_NOT_FOUND", Message: "订单不存在"}
ErrInsufficientStock = &BusinessError{Code: "INSUFFICIENT_STOCK", Message: "库存不足"}
)
然后用:
if errors.Is(err, ErrOrderNotFound) {
// 返回 404 或提示用户检查订单号
}
if errors.As(err, &BusinessError{}) {
// 统一提取 Code 和 Message 做响应包装
}
注意:
- 哨兵变量必须是导出的全局变量(如
ErrOrderNotFound),不能是局部var - 如果错误来自第三方库(如
pgx),用errors.As提取具体类型(如*pgconn.PgError)比字符串匹配更稳 - 不要滥用
fmt.Errorf("wrap: %w", err)—— 只在需要添加上下文时才用,且确保上游错误本身支持%w
HTTP handler 中错误响应要分层转换
handler 层不直接返回原始 error,也不把所有错误都转成 500。必须按错误类型映射状态码和响应体:
-
BusinessError→ 4xx 状态码(如ErrOrderNotFound→ 404,ErrInsufficientStock→ 400) -
SystemError→ 5xx 状态码(如 503 表示依赖服务不可用,500 表示未预期 panic) - 其他未分类错误(如
context.DeadlineExceeded)→ 显式映射为 408 或 504
一个轻量封装示例:
func WriteErrorResponse(w http.ResponseWriter, err error) {
var be *BusinessError
if errors.As(err, &be) {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]interface{}{
"code": be.Code,
"message": be.Message,
})
return
}
if errors.Is(err, context.DeadlineExceeded) {
w.WriteHeader(http.StatusRequestTimeout)
return
}
log.Printf("system error: %+v", err) // 记录完整错误链
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{"message": "服务暂时不可用"})
}
重点:
- 永远先尝试
errors.As检查业务错误,再 fallback 到系统错误判断 - 不要在 handler 里调用
log.Fatal或 panic —— 这会让整个服务崩溃 - 如果用了 Gin/Echo 等框架,别依赖其内置的
Error方法,它们默认不处理错误链
测试中必须覆盖错误路径和错误类型断言
很多团队只测“成功路径”,导致错误处理逻辑上线后才发现 errors.Is 总是 false。真实错误往往来自底层依赖(DB、RPC、HTTP client),必须模拟。
例如测试一个查询订单的 service 方法:
func TestGetOrder(t *testing.T) {
// mock db 返回 ErrOrderNotFound
mockDB := &MockOrderDB{Err: ErrOrderNotFound}
svc := NewOrderService(mockDB)
_, err := svc.GetOrder(context.Background(), "123")
if !errors.Is(err, ErrOrderNotFound) {
t.Fatal("expected ErrOrderNotFound, got", err)
}
}
容易忽略的点:
- mock 对象的
Err字段必须是同一哨兵变量(ErrOrderNotFound),不是新errors.New("not found") - 测试 timeout 场景时,用
context.WithTimeout并检查是否命中context.DeadlineExceeded - 集成测试中,故意断开 DB 连接,验证是否生成
SystemError而非裸露驱动错误
错误处理机制最脆弱的地方不在代码里,而在测试没跑过的分支和没 mock 到的底层错误路径。多花十分钟补全这些 case,比加十个监控指标更能防止线上故障。


















