recover必须写在拦截器里而非handler内部,因为grpc框架调用handler时不提供panic防护,handler内panic会直接导致goroutine终止、连接挂死;只有拦截器中defer recover才能兜底捕获并返回标准错误。

必须在每个拦截器最外层加 defer func() { recover() },且 Recovery 拦截器必须放在拦截器链最外层——否则 panic 会直接穿透到 net/http2 层,导致连接挂死、客户端收不到响应。
为什么 recover 必须写在拦截器里,而不是 handler 内部
gRPC 的 handler 是由框架调用的普通函数,它本身不带任何 panic 防护。一旦 handler 或其调用链中任意位置(比如鉴权逻辑、DB 查询、JSON 解析)发生 panic,若未被 recover,goroutine 会终止,但连接不会断开。后续请求可能卡在 context.DeadlineExceeded 或直接超时,服务看似存活实则不可用。
常见错误是只在业务方法里加 recover,比如:
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
defer func() { recover() }() // ❌ 错误:这里 recover 不起作用
panic("boom") // 这个 panic 仍会冒泡到拦截器外层
}
真正有效的 recover 只能出现在拦截器的 defer 中,且必须在调用 handler(ctx, req) 之前注册。
UnaryInterceptor 中 recover 的标准写法
以下是最小可行、生产可用的 recover 拦截器骨架,注意三点:命名返回值、status.Error 构造错误、日志打点不依赖 panic 值本身(因为 recover() 可能返回 nil):
-
defer func()必须在函数开头就注册,不能晚于handler调用 - 用
err = status.Error(codes.Internal, "internal error")而不是errors.New,确保客户端收到明确状态码 - 不要直接打印
r的原始值(可能是字符串、结构体甚至nil),建议用fmt.Sprintf("%v", r)统一格式化
func UnaryRecoveryInterceptor(logger *zap.Logger) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
defer func() {
if r := recover(); r != nil {
logger.Error("panic recovered in unary interceptor",
zap.String("method", info.FullMethod),
zap.String("panic", fmt.Sprintf("%v", r)),
zap.String("stack", string(debug.Stack())),
)
err = status.Error(codes.Internal, "internal error")
}
}()
return handler(ctx, req)
}
}
StreamInterceptor 的 recover 更容易漏掉
流式拦截器签名不同,没有直接的 handler 调用,而是传入 grpc.StreamHandler 并手动调用 handler(srv, ss)。很多团队只写了 Unary 的 recover,结果流式接口(如 Subscribe、Chat)一 panic 就整条连接崩掉。
诊断并恢复通过 SSH 隧道连接的 OpenClaw 节点。用于解决配对必需错误、隧道冲突、远程端点错误以及 SSH 目标配置错误等问题。
关键点:
- 必须对
handler(srv, ss)包一层defer recover,不能只包ss.RecvMsg或ss.SendMsg - 流式场景下 panic 可能发生在任意一次
RecvMsg或SendMsg,但 recover 只需在 handler 调用处捕获一次即可,因为整个流生命周期都在这个 handler 内 - 若需细粒度控制(比如只 recover 接收阶段),得自己封装
grpc.ServerStream,但通常没必要
func StreamRecoveryInterceptor(logger *zap.Logger) grpc.StreamServerInterceptor {
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
defer func() {
if r := recover(); r != nil {
logger.Error("panic recovered in stream interceptor",
zap.String("method", info.FullMethod),
zap.String("panic", fmt.Sprintf("%v", r)),
)
}
}()
return handler(srv, ss)
}
}
recover 放错顺序会导致服务彻底瘫痪
拦截器链顺序决定 panic 是否能被捕获。如果把 AuthInterceptor 放在 RecoveryInterceptor 外层,Auth 里一个空指针就会 kill 整个进程——因为 panic 在到达 Recovery 前就已上浮到 goroutine 根层。
正确顺序(从外到内):
-
RecoveryInterceptor(最外层,兜底) -
TracingInterceptor(需先建好 trace_id,供后续日志/鉴权使用) -
AuthInterceptor/LoggingInterceptor/TimeoutInterceptor
用 go-grpc-middleware 链式注册时,顺序就是参数列表顺序:
grpc.NewServer(
grpc.ChainUnaryInterceptor(
UnaryRecoveryInterceptor(logger),
UnaryTracingInterceptor(),
UnaryAuthInterceptor(authFunc),
UnaryLoggingInterceptor(logger),
),
)
别以为“recover 一次就够了”——每个拦截器都该有自己独立的 defer recover,但 Recovery 必须最外层;中间拦截器里的 recover 只是防御性补充,不能替代主 recovery。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










