错误码必须用结构体封装,定义含Code、HTTPStatus、Message字段的error实现,通过统一工厂函数创建并包装底层错误,集中管理于独立module且CI校验,gRPC/HTTP透传需分离传输层与业务语义。

错误码必须用结构体封装,不能用 int 或字符串硬编码
直接写 return fmt.Errorf("user not found: %s", id) 或返回裸 int 错误码(如 2001)会导致下游无法类型断言、日志无法结构化解析、前端只能字符串匹配——一改文案或加个空格就崩。
正确做法是定义一个实现了 error 接口的结构体,至少包含三个字段:Code(业务语义标识,如 "USR-001")、HTTPStatus(协议层状态码)、Message(用户可见提示)。不要在 Error() 方法里拼接敏感信息(如用户 ID、SQL 片段)。
-
Code必须是字符串类型,带服务前缀(如USR、ORD),禁止纯数字;全局唯一,不随语言/服务变化 - 结构体要实现
Unwrap() error,否则errors.Is(err, ErrUserNotFound)永远返回false - 若需支持
errors.As(),还要实现As(target interface{}) bool,通常只处理目标为同类型指针的情况
所有错误创建必须走统一工厂函数,禁用 errors.New 和 fmt.Errorf
手写 errors.New("db timeout") 或 fmt.Errorf("user %s not found", id) 会丢失 traceID、无法注入服务名、下游拿不到 Code 字段——等于把错误“裸抛”出去。
工厂函数(如 NewError(code, msg, fields))才是唯一合法入口,它负责:
立即学习“go语言免费学习笔记(深入)”;
- 从入参
context.Context提取traceID和service name,绝不使用context.Background() - 校验
code是否在合法范围内,非法值应panic或 fallback 到通用错误(如"SYS-001") - 对底层错误(如
sql.ErrNoRows)用errors.Wrap()包装,保留堆栈,同时升级为业务语义
示例:return user.NewErrUserNotFound().Wrap(dbErr),不是 fmt.Errorf("USR-001: %w", dbErr)——后者会让 errors.Is() 失效。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
gRPC 和 HTTP 场景下错误码透传方式完全不同
gRPC 的 status.Code 是传输层语义(如 codes.Unavailable),绝不能映射业务含义(比如把“余额不足”塞进 codes.ResourceExhausted);HTTP 的 404 状态码也不该由业务逻辑触发——否则网关重试、前端跳转全乱套。
实操要点:
- gRPC 响应 message 中显式定义
string error_code = 1;和string error_message = 2;字段 - 服务端一律用
status.OK返回业务错误,仅用status.Error()处理真正的传输失败(如超时、连接中断) - HTTP 网关中间件检查
error_code字段:非空时固定返回200 OK,把错误包进 JSON body;绝不依赖grpc-gateway自动映射状态码
错误码必须随响应体一起序列化,不能只打日志——调用链断了,监控就聚合不了。
错误码必须集中定义在公共 module,且强制 CI 校验
各服务各自定义 const ErrUserNotFound = 2001,不出三个月就会撞码、漏文档、改一个要 grep 全库。更糟的是,proto 文件里写裸数字,生成代码后根本没法跨语言对齐。
落地动作:
- 所有错误码定义在独立 Go module(如
github.com/yourorg/errors),用const+iota按域分组:AuthErrorCode、UserErrorCode - proto 中通过
enum引用该 module 导出的值,靠go_packageoption 关联生成代码 - CI 脚本在 PR 提交时校验:每个 enum 值是否在 errors module 中存在,且注释非空——漏写说明的禁止合入
前缀(如 USR)必须在公司级文档注册,避免冲突;后缀数字从 001 开始,对齐便于 grep 和排序。

















