错误码必须分层唯一且全局统一,采用SSS-TT-NNN格式定义在公共module中;所有错误须经工厂函数创建并实现StatusCoder接口;message支持i18n,reason仅用于日志;traceID须从入参context提取。

错误码必须分层唯一,不能靠字符串匹配判断
微服务里最常见也最危险的错误处理方式,就是用 strings.Contains(err.Error(), "not found") 这类逻辑做分支——一旦日志加了前缀、message被翻译、甚至只是拼写多空格,整个判断就失效。真正可靠的方式是让错误本身携带可识别的语义:每个错误码全局唯一、按模块分段(如 101-02-001 表示用户服务的「资源未找到」),且所有服务共用同一套编号规则。
- 推荐格式:
SSS-TT-NNN(3位服务号 + 2位类型 + 3位序号),避免用iota连续编号——跨服务时极易冲突 - 错误码不随语言/服务变化,前端、网关、监控系统都依赖它做决策,所以必须定义在公共 module(如
common/errors)中 - 禁止在 HTTP 响应体里塞
error: "db timeout"字段——这违反协议层约定,gRPC 应用status.Error(),HTTP 应返回标准 JSON 错误体
构造函数必须统一入口,禁止裸写 fmt.Errorf 或 errors.New
业务代码里随手写 return fmt.Errorf("user %s not found", id),看似简单,实则埋下三个坑:无法提取 Code、丢失 traceID、下游无法结构化解析。所有错误创建必须走封装好的工厂函数,比如 NewError(code, msg, fields),它负责注入上下文、校验合法性、并确保实现对应协议接口。
-
NewError内部应从context.Context提取traceID和service name,而不是硬编码或用context.Background() - 对底层 error(如数据库驱动错误)要用
Wrap包装,保留原始堆栈,同时升级为业务语义:errors.Wrap(dbErr, ErrUserNotFound) - 构造函数需校验
code是否在合法区间,非法值应 panic 或 fallback 到通用错误(如50001),而非静默忽略
HTTP 中间件要区分 ErrorCoder 和非结构化 error
中间件不是“兜底 catch all”的地方,而是分类响应的枢纽。它必须能明确区分两类错误:实现了 StatusCoder 接口的业务错误(可取 StatusCode()),和未封装的底层 error(如 json.SyntaxError、io.EOF)。前者走标准 JSON 响应,后者统一转为 500 并记录日志,绝不暴露细节。
- 定义接口:
type StatusCoder interface { StatusCode() int },让*AppError或*bizError实现它 - 中间件里不要用
recover()捕获所有 panic 来替代显式错误返回——这会让参数校验失败和空指针崩溃混为一谈,掩盖真实问题位置 - 响应体字段要分离用途:
code是业务码(如2001),status是 HTTP 状态码(如404),message仅作前端提示,details仅用于调试且生产环境脱敏
错误 message 必须支持 i18n,但 reason 绝不返回给前端
你看到的 "user not found" 是日志里的 reason,不是给用户的提示。真正的用户文案应该由调用方根据 code 查多语言表生成,比如请求头带 Accept-Language: zh-CN,就返回 "用户不存在";开发时还可强制切英文便于排查。
立即学习“go语言免费学习笔记(深入)”;
-
reason存在错误码元信息映射表里(如ErrCodeMeta[2001].Reason),只用于日志和 debug -
message字段在ErrorCoder.Message()方法中动态取值,不硬编码在错误结构体里 - 前端不应解析 error 字符串,而应查
code映射表——这样换语言、改文案、加新 locale 都不影响 API 兼容性
最容易被忽略的是 traceID 的传递时机:它必须从入参 context.Context 中提取,而不是在 NewError 里调用 trace.FromContext(context.Background())——后者永远拿不到真实链路 ID。错误一旦离开当前 goroutine,上下文就断了,再补就晚了。










