统一错误响应需贯穿错误全生命周期:构造用工厂函数、code用预定义常量、包装保留堆栈、中间件分类处理、跨服务结构化序列化、traceid全程传递、错误码分层唯一且不变。

统一错误响应不是加个中间件就能搞定的事——它要求错误从生成、包装、透传到序列化,每一步都遵循同一套契约。否则中间件再漂亮,上游返回的 fmt.Errorf("not found") 依然会让前端抓瞎、日志无法关联、重试逻辑失效。
错误构造必须走工厂函数,禁用裸 fmt.Errorf 和 errors.New
业务代码里直接写 return fmt.Errorf("user %s not found", id) 看似省事,实则切断了错误的语义链:没 Code、没 traceID、没 HTTP status,下游只能靠字符串匹配,一改 message 就崩。
- 所有错误必须经由统一工厂函数创建,例如
NewError(code, msg, fields),它内部会从context.Context提取traceID和服务名 -
code必须来自公共 module(如common/errors)里的预定义常量,格式推荐SSS-TT-NNN(如"101-02-001"),禁止用iota或硬编码数字 - 底层 error(如数据库驱动报错)要用
errors.Wrap(dbErr, ErrUserNotFound)包装,保留原始堆栈,同时升级为业务语义 - 工厂函数需校验
code合法性:非法值应 panic 或 fallback 到通用错误码(如50001),不静默忽略
HTTP 中间件只处理两类错误:实现了 StatusCoder 的和没实现的
中间件不是“兜底 catch all”的地方,而是分类响应的枢纽。它必须能明确区分结构化错误和原始 error,不能把 json.SyntaxError 和业务 ErrUserNotFound 混成一个 500 返回。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 定义接口:
type StatusCoder interface { StatusCode() int },让业务错误类型(如*AppError)实现它 - 中间件中先尝试
errors.As(err, &statusCoder),若成功则取statusCoder.StatusCode()并返回标准 JSON 错误体 - 若失败(即未封装的底层 error),统一转为
500,记录完整堆栈但绝不暴露细节给响应体 - 严禁在中间件里用
recover()捕获 panic 来替代显式错误返回——这会让参数校验失败和空指针崩溃混为一谈
跨服务调用时,错误必须结构化序列化,禁用透传原始 error 字符串
RPC 响应体里塞 error: "db timeout" 违反协议层约定,gRPC 应用 status.Error(),HTTP 应返回标准 JSON 错误体 + 状态码。否则下游无法解析语义,重试策略、熔断判断全失效。
- gRPC 场景:用
status.WithDetails()将自定义错误结构(含Code、Message、TraceID)作为 Protobuf 消息附加,客户端可结构化解析 - HTTP 场景:错误体必须是标准 JSON,字段如
{"code":"101-02-001","message":"用户不存在","trace_id":"xxx"},HTTP 状态码按语义映射(如404对应资源未找到) -
reason字段仅用于日志,绝不出现在响应体中;message面向用户,支持 i18n - 所有跨服务请求必须携带
traceID,通过context.Context传递,并在每个服务日志中打点,否则错误无法串联定位
错误码设计要分层唯一,且不随语言/服务变化
错误码是微服务间最稳定的契约之一,前端、网关、监控系统都依赖它做决策。一旦定义,就不能因某个服务重构或换语言而变动。
- 格式强制用
SSS-TT-NNN(3位服务号+2位类型+3位序号),比如"201-01-003"表示订单服务的「库存不足」 - code 必须定义在公共 module(如
common/errors)中,所有服务共用同一份编号规则,避免跨服务冲突 - 禁止用字符串枚举(如
"not_found"),难维护且无法校验;推荐int const+ 映射表,运行时可校验合法性 - 错误码不承载 HTTP 状态码语义,
code是业务标识,HTTP status是协议层行为,两者映射关系应在网关或中间件中集中管理
真正容易被忽略的是错误的「生命周期一致性」:从 handler 里第一次调用 NewError,到跨服务传输时的序列化/反序列化,再到中间件输出前的最后校验——每一步都得确保 Code 不变、traceID 不丢、message 不被二次格式化。漏掉任意一环,统一就只是表面功夫。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










