gRPC拦截器必须分unary和stream两种实现,因Server不支持统一拦截;error需用status.Errorf构造并在拦截器中解包转状态码;拦截器须recover panic并返回codes.Internal;日志和trace ID必须通过context传递而非全局变量。

gRPC 拦截器必须分 unary 和 stream 两种写法
Go 的 grpc.Server 不支持统一拦截所有 RPC 类型,UnaryInterceptor 和 StreamInterceptor 必须分开注册,漏掉任意一个,对应类型的请求就完全绕过你的逻辑。
常见错误是只加了 UnaryInterceptor,结果发现流式接口(比如 Subscribe)的日志、鉴权、超时控制全失效。这是因为 unary 拦截器对 ClientStream/ServerStream 完全不生效。
实操建议:
- 如果项目里同时用到 unary 和 stream,两个拦截器都得写,且逻辑尽量复用(比如把公共校验抽成函数)
-
StreamInterceptor的签名更复杂,需手动调用ss.RecvMsg()和ss.SendMsg()前后埋点,不能像 unary 那样直接 wraphandler - 测试时务必分别起一个 unary 方法和一个 stream 方法验证,别只测一种
error 从 handler 返回后不会自动转成 gRPC 状态码
Go gRPC 默认把 error 当作 codes.Unknown 发给客户端,哪怕你返回的是 status.Error(codes.NotFound, "user not found"),如果没在拦截器里显式处理,客户端收到的仍是 UNKNOWN 而不是 NOT_FOUND。
根本原因是:gRPC Go 的底层不解析 error 类型,它只原样透传 error.Error() 字符串,状态码信息被丢弃了。
实操建议:
- 统一用
status.Errorf(code, format, args...)构造 error,别手写errors.New()或fmt.Errorf() - 在
UnaryInterceptor里用status.FromError(err)解包,再调用err.(interface{ GRPCStatus() *status.Status }).GRPCStatus()获取真实 code - 若 error 不是
*status.Status类型(比如 nil 或自定义 error),默认 fallback 到codes.Internal,避免暴露堆栈
拦截器里 recover panic 是必须的,否则会 kill 整个 server
gRPC handler 中一旦 panic,且没有在拦截器里 recover,goroutine 会终止,但连接不断,后续请求可能卡死或触发 context deadline exceeded——这不是超时配置问题,是 server 端 panic 后未清理资源导致的状态错乱。
Go gRPC 的 Server 默认不 recover,panic 会直接向上冒泡到 net/http2 层,最终表现为客户端收不到响应、连接重置。
实操建议:
- 每个拦截器最外层加
defer func() { if r := recover(); r != nil { log.Printf("panic recovered: %v", r) } }() - recover 后建议返回
status.Error(codes.Internal, "internal error"),而不是原 panic message,防止敏感信息泄露 - 不要试图在 recover 后继续执行 handler —— context 可能已 cancel,stream 可能已 close,继续调用
SendMsg会 panic
日志和 trace ID 必须从 context 里取,不能依赖全局变量
多个 RPC 并发时,如果在拦截器里用全局变量存 request ID 或 logger,会互相覆盖。gRPC 的每个 handler 运行在独立 goroutine,但全局变量是共享的。
正确方式是把 trace ID 注入 ctx,再通过 ctx.Value() 或更推荐的 grpc_ctxtags.Extract(ctx)(配合 grpc_zap)传递。
实操建议:
- 在 unary 拦截器开头生成 trace ID(如用
uuid.New().String()),然后ctx = context.WithValue(ctx, keyTraceID, id) - handler 内部一律用
ctx.Value(keyTraceID)取 ID,别自己 new - stream 拦截器要更小心:每个
RecvMsg/SendMsg调用都应带上当前 ctx(它可能已被中间件修改),别复用初始 ctx
真正难的是 stream 场景下上下文的生命周期管理——RecvMsg 可能阻塞几秒,期间 ctx 可能 timeout,这时候再往里面塞字段或改值,容易引发竞态。别图省事用 map 存 ID 映射,老老实实靠 context 传递最稳。

















