不能直接用fmt.errorf返回业务错误,因其生成纯字符串error、无结构化字段,导致中间件无法获取code或httpstatus,日志无法溯源;且%w包装后errors.is失效,必须使用实现errorcode()等接口的自定义错误类型并经工厂函数创建。

为什么不能直接用 fmt.Errorf 返回业务错误
因为 fmt.Errorf 生成的是纯字符串 error,没有结构化字段,中间件拿不到 Code 或 HTTPStatus,日志里也查不到错误来源模块。一旦有人写 return fmt.Errorf("user not found: %w", dbErr),上游就彻底丢失错误码语义——连 errors.Is(err, user.ErrNotFound) 都失效。
真正要拦截和映射的,是实现了 ErrorCoder 接口(或至少含 ErrorCode() 方法)的自定义错误类型,比如:
type UserError struct {
code int
msg string
httpStatus int
}
func (e *UserError) ErrorCode() int { return e.code }
func (e *UserError) Error() string { return e.msg }
- 所有业务错误必须通过工厂函数创建,例如
user.NewNotFoundErr(),禁止裸&UserError{} - 底层依赖(DB、gRPC、HTTP)返回的 error 必须用
errors.Wrap()或自定义Wrap()包装,而不是直接return err -
ErrorCode()方法名必须固定,中间件、日志器、响应层才好统一提取
如何让 Code 和 HTTPStatus 解耦又可映射
错误码 Code 是业务唯一标识(如 10001 表示用户不存在),而 HTTPStatus 是协议层状态(如 404)。二者硬绑定会导致改状态码就得动所有错误定义。
推荐做法是建一张轻量映射表,在中间件序列化响应前做一次查表:
var statusCodeMap = map[int]int{
10001: 404, // user not found
10002: 400, // user param invalid
50001: 500, // system internal error
}
- 映射表只在初始化时加载一次,线程安全,不支持运行时修改
- 未显式映射的
Code默认 fallback 到500,避免因遗漏导致返回空状态 - 同一个
Code在 gRPC 场景下可能映射为codes.NotFound,而在 HTTP 层才查表转成404—— 这才是真正的解耦
HTTP 中间件和 gRPC 拦截器必须都配全
recovery.UnaryServerInterceptor 不能省,否则 panic 会直接崩掉协程,客户端收到 status.Code = Unknown 或连接中断,日志里可能连堆栈都没有。
但只加 UnaryServerInterceptor 不够:流式方法(如 rpc Chat(stream ChatMessage) returns (stream ChatMessage))照样崩溃。
- 必须同时配置
recovery.UnaryServerInterceptor和recovery.StreamServerInterceptor -
WithRecoveryHandler里返回的必须是status.Error(),不是普通error;否则客户端收不到标准错误码 - recover 函数里不能
log.Fatal或再panic,否则把拦截器自己干掉了 - HTTP 中间件同理:
defer func() { if r := recover(); r != nil { ... } }()必须包裹整个 handler 执行,且c.Abort()要及时调用
跨服务调用时错误语义怎么保持一致
下游服务返回 Code=10001,上游直接透传给前端?不行。不同服务对同一错误码的理解可能不同,网关或前端也不该直读下游码。
正确做法是:在调用链中保持错误“可识别、可转换、可追溯”:
- 封装客户端(HTTP/gRPC),在
DoRequest或Invoke后统一检查状态,并将原始错误映射为本服务预设的CodeError类型 - 用
context.WithValue或metadata传递 traceID 和原始错误码(如x-original-code: 10001),供网关做降级或审计 - 日志中必须记录
err.Unwrap()链,否则查不到是 DB 超时还是 Redis 连接失败 - 禁止在任意一层用
fmt.Errorf("%w", err)替换原始 error —— 除非你确定上层能穿透Unwrap()拿到ErrorCode()
最常被忽略的一点:错误码的“域”必须明确。比如用户服务用 1001–1999,订单服务用 2001–2999,全局冲突比重复定义更难排查。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











