不能直接用 errors.new 或 fmt.errorf 做微服务错误码,因为它们生成的错误无结构化字段,无法统一提取错误码、http 状态码和日志上下文,下游只能字符串匹配判断错误类型,极难维护且易崩。

为什么不能直接用 errors.New 或 fmt.Errorf 做微服务错误码
因为它们生成的错误没有结构化字段,无法统一提取错误码、HTTP 状态码、日志上下文;下游服务或前端只能靠字符串匹配判断错误类型,一改就崩。
- 常见错误现象:
if strings.Contains(err.Error(), "not found")—— 这种写法在多语言、多版本、多服务间极难维护 - 真实使用场景:用户登录失败要返回
401,但数据库连不上又要返回503,两者都可能触发err != nil,但语义完全不同 - 性能影响:每次
fmt.Errorf("user not found: %s", uid)都会分配新字符串,而结构化错误可复用底层 error 实例
定义全局错误码类型必须包含三个核心字段
不是简单建个 map[string]int 就完事。Go 的 error 接口只认 Error() string 方法,所以得自己实现满足接口又带元数据的类型。
- 必须有
Code字段(如"USER_NOT_FOUND"),用于业务逻辑分支判断 - 必须有
HTTPStatus字段(如404),供 HTTP 中间件自动映射响应状态 - 必须实现
Error() string,且内容不含敏感信息(如不拼接原始 SQL 或用户邮箱) - 示例结构体:
type CodeError struct { Code string HTTPStatus int Message string // 用户可见提示,非 debug 信息 }
如何让 errors.Is 和 errors.As 正常工作
Go 1.13+ 的错误链机制默认只认 Unwrap(),如果你的自定义 error 没实现它,errors.Is(err, ErrUserNotFound) 就永远返回 false。
- 必须实现
Unwrap() error方法,返回nil(表示无嵌套)或底层原始 error(如sql.ErrNoRows) - 若需支持类型断言,还要实现
As(target interface{}) bool,通常只需判断target是否为指针并赋值 - 容易踩的坑:
CodeError如果内嵌了另一个 error(比如包装了io.EOF),但没正确实现Unwrap(),上层调用errors.Is(err, io.EOF)就失效 - 推荐写法:
func (e *CodeError) Unwrap() error { return e.cause } func (e *CodeError) As(target interface{}) bool { if p, ok := target.(*CodeError); ok { *p = *e return true } return false }
错误码注册和 HTTP 中间件怎么联动
错误码本身是静态的,但它的 HTTP 映射关系必须能被中间件读取,否则每个 handler 都要手动写 switch e.Code。
立即学习“go语言免费学习笔记(深入)”;
- 建议用全局注册表:
var codeRegistry = map[string]struct{ Code string; Status int }{},在init()里注册所有码 - 中间件中统一处理:
if ce, ok := errors.Cause(err).(*CodeError); ok { w.WriteHeader(ce.HTTPStatus) } - 兼容性注意:如果用了
github.com/pkg/errors或golang.org/x/xerrors,记得用errors.Cause()而非直接断言,避免被中间 error 包装层挡住 - 别漏掉日志字段:在中间件记录错误时,应显式输出
code=USER_NOT_FOUND http_status=404,而不是只打err.Error()
最麻烦的其实是跨服务传播——gRPC 里要用 Status,HTTP 里用 JSON,内部调用又要保持 Go 原生 error 接口。三者字段对齐稍有偏差,下游就收不到预期码。










