所有向外暴露的错误必须结构化封装,携带可序列化元信息,禁止裸抛errors.New或fmt.Errorf;gRPC用status.Error()、HTTP返回JSON+状态码,须做协议映射;错误判断依赖code而非字符串匹配,panic仅限不可恢复场景且需goroutine级recover。

错误不能裸抛,必须结构化封装
微服务里直接 return errors.New("db timeout") 或 fmt.Errorf("user not found: %s", id) 是危险操作:下游无法提取 code、日志没 traceID、前端没法做差异化提示。所有向外暴露的错误必须携带可序列化的元信息。
正确做法是统一用工厂函数构造,比如:NewError(ErrUserNotFound, "user not found", zap.String("user_id", id))。这个函数内部会从 context.Context 提取 traceID,校验 code 是否在合法区间(如 1001–1999 属用户域),并确保实现 StatusCoder 接口。
- 禁止在 error message 里拼接敏感字段(如 token、手机号),message 只用于前端展示,敏感上下文走独立日志字段
- 底层驱动错误(如
sql.ErrNoRows)必须用%w包装:return fmt.Errorf("failed to query user: %w", err),否则errors.Is(err, sql.ErrNoRows)判断失效 - HTTP 响应体里不要塞
error: "pq: duplicate key"字段——这违反协议层约定,应转成标准 JSON 错误体 + 状态码
gRPC 和 HTTP 边界必须做错误映射
同一业务错误,在 gRPC 和 HTTP 场景下暴露方式完全不同:gRPC 用 status.Error(),HTTP 返回 JSON + 状态码。不转换就直接透传,调用方根本解析不了。
例如一个「用户不存在」错误,gRPC server 应写:return status.Error(codes.NotFound, "user not found");HTTP handler 则要转成:http.StatusNotFound + {"code": 1002, "message": "user not found"}。
立即学习“go语言免费学习笔记(深入)”;
- gRPC 客户端必须用
status.Code(err) == codes.NotFound判断,而不是strings.Contains(err.Error(), "not found")—— 文案一改逻辑就崩 - HTTP 中间件需区分两类错误:实现了
StatusCoder的业务错误(取.StatusCode()),和未封装的底层 error(如json.SyntaxError),后者统一打 500 并脱敏记录 - 不要在 handler 里写
return err就完事,那只会让客户端收到 500 却不知错在哪一层
跨服务错误传播靠 code,不是字符串匹配
服务 A 调用服务 B,B 返回了 status.Error(codes.PermissionDenied, "..."),A 如果用 err.Error() 做字符串判断,等于把错误语义和文案强耦合。一旦 B 改了提示语,A 的降级逻辑就失效。
真正可靠的方式是提取语义码:if status.Code(err) == codes.PermissionDenied,或解析 JSON 响应里的 code 字段(如 1003),再查公共错误码表做路由决策。
- 错误码必须全局唯一、分层定义,推荐格式
SSS-TT-NNN(如101-02-001表示用户服务的「权限不足」),定义在公共 module,所有服务共用 - 禁止用
iota在各服务里各自编号——跨服务极易冲突 - 前端、网关、监控系统都依赖
code做自动处理,它不能随语言/服务变化,message 和 reason 只用于日志和展示
panic 只能用于不可恢复场景,且必须限定 goroutine
在 HTTP handler 或 gRPC 方法里写 panic("timeout"),会导致整个服务进程退出,K8s 持续重启 Pod。recover 只对同 goroutine 有效,而 http.ServeMux 和 grpc.Server 都会在新 goroutine 中调用你的业务逻辑。
必须显式包裹入口函数:func(w http.ResponseWriter, r *http.Request) { defer errorHandler(w); handler(w, r) },或在 gRPC interceptor 里加 defer func() { if r := recover(); r != nil { ... } }()。
- log.Fatal 和 panic 不是错误处理手段,只适用于程序启动失败等真正不可恢复的场景
- 中间件里不要用
recover()替代显式 error 返回——这会让参数校验失败和空指针崩溃混为一谈 - 错误处理逻辑自身不可 panic,否则形成嵌套崩溃,日志链断裂
%w 忘写、或者 HTTP 中间件把未实现 StatusCoder 的底层 error 当作业务错误返回——这些点不盯紧,可观测性就形同虚设。


















