panic recovery 必须置于拦截器链最外层,否则上游拦截器(如tracing、auth)触发空指针等panic将逃逸出捕获范围,导致进程崩溃;需为unary和stream分别配置首置的recovery拦截器,并统一返回status.error(codes.internal, "internal error")。

panic recovery 必须放在拦截器链最外层
服务端一旦 panic 且没被 recover,整个 goroutine 会终止,连接不关闭,后续请求卡在 context deadline exceeded 或直接断连——这不是配置问题,是进程级崩溃前的残态表现。Recovery 拦截器如果放在 Tracing 或 Auth 后面,前面任一拦截器触发 nil pointer dereference,panic 就逃逸出 recover 范围,整机退出。
正确做法是让 UnaryRecoveryInterceptor 成为链上第一个执行的 unary 拦截器:
- 用
grpc_middleware.ChainUnaryServer()组合时,把它排在参数列表最前面 - 不要手动嵌套调用 handler —— recovery 的 defer 必须包裹整个 handler 调用链
- stream 拦截器同理,必须有独立的
StreamServerInterceptor版本,且同样置于链首
recover 后怎么返回 gRPC 可识别的错误
recover 捕获到 panic 后,不能直接 return fmt.Errorf("panic: %v", r),否则客户端收到的是 codes.Unknown,status.FromError 解析失败。
必须统一转成 status.Error(codes.Internal, "internal error"):
- 避免暴露堆栈:
debug.Stack()只记日志,不塞进 error message - 若需区分 panic 类型(如 DB 连接中断 vs 空指针),可在日志里打 tag,error 本身保持通用
- stream 拦截器中 recover 后要主动调用
ss.CloseSend(),否则客户端可能 hang 在 Recv()
metadata 提取失败导致的隐性 panic 怎么防
很多 panic 其实来自拦截器里对 metadata.FromIncomingContext(ctx) 的误用,比如没检查 ok 就直接取 md.Get("authorization")[0],而实际 md 是空或 key 不存在。
高频踩坑点:
-
FromIncomingContext和FromOutgoingContext混用:服务端只能用前者 - key 名必须小写:
"authorization"✅,"Authorization"❌ -
md.Get("auth")返回[]string,取值前必须判空:if len(token) == 0 || token[0] == "" - 别在 recover 外层做 metadata 解析——万一这里 panic,recover 就失效了
为什么只注册 UnaryInterceptor,Stream 接口却完全没反应
因为 grpc.UnaryInterceptor 对 streaming RPC 完全无效。ClientStream/ServerStream 的生命周期不由 unary handler 控制,panic 发生在 RecvMsg() 或 SendMsg() 内部时,unary 拦截器根本不会执行。
必须单独实现 stream 恢复逻辑:
- 签名是
func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error - 需要包装
ss,重写RecvMsg和SendMsg方法,在里面加 defer recover - go-grpc-middleware 的
ChainStreamServer是必需的,手写链容易漏掉handler(srv, wrappedStream)导致流卡死
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











