必须在每个服务入口显式加 defer recover(),gRPC 需同时注册 Unary 和 Stream 两种 Recovery 拦截器,错误须用 status.Errorf 构造,recover 中禁用 log.Fatal/panic,HTTP 中间件需覆盖全链路 panic,错误码判断必须用 status.Code() 而非 errors.Is,跨服务需解包重封装错误。

必须在每个服务入口显式加 defer recover(),否则 panic 会直接杀死 goroutine,客户端看到的不是错误而是连接中断或 StatusUnknown。
gRPC 服务必须配齐 Unary 和 Stream 两种 Recovery 拦截器
只注册 recovery.UnaryServerInterceptor 是不够的——流式方法(如 rpc StreamData(stream Request) returns (stream Response))照样崩溃,因为流式调用走的是另一条执行路径。
- 两个拦截器都要注册:
grpc.UnaryInterceptor(recovery.UnaryServerInterceptor(...))和grpc.StreamInterceptor(recovery.StreamServerInterceptor(...)) - 自定义
WithRecoveryHandler时,recover函数里必须用status.Errorf构造错误,不能返回裸error;否则客户端调用status.FromError(err)会得到codes.Unknown,无法做码级判断 - 别在 handler 里直接
panic("xxx"),更不要在 recover 里再log.Fatal或panic,那等于把拦截器自己干掉了
HTTP 服务用中间件统一 recover,但别依赖框架默认行为
Gin/Echo 的 Recovery() 中间件看似开箱即用,但实际容易漏掉两类问题:
- 它只捕获 handler 内 panic,不处理中间件链中上游 panic(比如鉴权中间件里空指针)——得确保所有中间件都包裹
defer recover() - 返回的
500响应体格式和业务错误不一致,建议统一用自定义结构体(如{Code: 500, Message: "Internal error"}),避免前端解析错乱 - 别在 recover 里打全量堆栈到日志(
debug.PrintStack()),生产环境应提取关键帧 + traceID 上报,防止日志刷爆磁盘
错误传递必须用 status.Code(),别用 errors.Is 直接比对原始 error
gRPC 错误经过 wire 传输后,原始 error 类型丢失,errors.Is(err, myErr) 总是 false。正确解法是:
- 服务端一律用
status.Errorf(codes.InvalidArgument, "xxx"),别用fmt.Errorf - 客户端必须先解包:
st, ok := status.FromError(err),再用st.Code()判断 - 若需透传结构化详情(比如字段校验失败),得配合
status.WithDetails+errdetails.BadRequest,而不是拼字符串塞进 message 字段 - 跨服务调用时,B 返回
codes.NotFound,A 不能直接return err给上游——必须status.FromError解包后再status.Errorf(st.Code(), ...)重封装,否则错误码降级为codes.Internal
最常被跳过的环节是:流式 RPC 的 Recovery 拦截器、错误码透传时的二次封装、以及 recover 后的日志裁剪——这三处一漏,可观测性和下游容错能力就断层了。


















