gRPC拦截器本质既非单纯的UnaryServerInterceptor也非StreamServerInterceptor,而是二者并存的独立类型:前者处理一元调用,后者处理流式调用,签名不同、不可互换,必须分别注册且各自实现对应逻辑。

gRPC拦截器本质是UnaryServerInterceptor还是StreamServerInterceptor
Go 的 gRPC 拦截器分两类:针对普通 RPC(一元调用)的 UnaryServerInterceptor,和针对流式调用(如 stream 方法)的 StreamServerInterceptor。两者签名不同、不能混用。你写一个函数想同时处理两种调用,必须分别注册——gRPC 不会自动降级或兜底。
常见错误是只实现了 UnaryServerInterceptor,但服务里用了 server.StreamingMethod(...),结果拦截器完全不生效,日志/鉴权/超时全失效,还查不出原因。
-
UnaryServerInterceptor接收ctx、req、info和handler,返回resp和err -
StreamServerInterceptor接收srv、ss(grpc.ServerStream)、info和handler,无返回值,需手动调用handler(srv, ss) - 流式拦截器中无法直接读取请求体(
ss.RecvMsg()可能阻塞),需要包装ServerStream实现透传或提前解析
如何注册多个拦截器并控制执行顺序
gRPC Go 不支持“中间件链”语法(比如 WithInterceptors(a, b, c)),而是把所有拦截器按参数顺序拼成一个闭包链。先写的拦截器最外层,后写的在内层——这和 HTTP 中间件的洋葱模型一致,但容易看反。
例如:grpc.UnaryInterceptor(chain(a, b, c)),实际执行顺序是 a → b → c → handler;如果 c 是日志,a 是鉴权,那日志里就看不到鉴权失败的细节,因为 c 根本没被执行。
立即学习“go语言免费学习笔记(深入)”;
- 用
grpc_middleware.ChainUnaryServer(a, b, c)(来自github.com/grpc-ecosystem/go-grpc-middleware)更清晰,语义明确 - 自定义链式函数时,注意每个拦截器必须显式调用
next(...),漏掉就中断整个链 - 不要在拦截器里 recover panic —— gRPC 默认已捕获并转为
codes.Unknown错误,重复 recover 可能掩盖真实堆栈
拦截器里怎么安全获取和修改 metadata
metadata 在拦截器中是只读副本,grpc.Peer、grpc.Method 等信息从 info 参数拿,而客户端传来的 header(如 authorization)必须从 grpc.RequestMetadata 里提取,不是直接读 ctx.Value。
修改 metadata 需通过 grpc.SetHeader 或 grpc.SendHeader,且只能在 handler 执行前调用;若 handler 已返回,再调用会 panic。
- 读取 token:
md, ok := metadata.FromIncomingContext(ctx),然后md["authorization"] - 写入响应 header:
grpc.SetHeader(ctx, metadata.Pairs("x-request-id", reqID)) - 不要用
metadata.AppendToOutgoingContext在 server 拦截器里——那是给 client 拦截器用的 - metadata key 默认小写并自动归一化,
"Authorization"和"authorization"读出来是一样的
为什么 context.WithTimeout 在拦截器里经常失效
根本原因是:gRPC 底层已经用 ctx 的 deadline 控制了网络层超时,你在拦截器里套一层 context.WithTimeout,只是给 handler 新建了个子 ctx,但 handler 内部若直接用原始 ctx(比如传给数据库驱动),那个 timeout 就被绕过了。
更隐蔽的问题是,gRPC 的 ctx.Deadline() 返回的是绝对时间,而很多业务代码习惯用相对 timeout(如 time.Second * 5),两者混用会导致实际超时时间错乱。
- 正确做法:统一用
ctx原生 deadline,通过ctx.Done()监听取消,而不是重设 timeout - 若必须加额外超时(如 DB 查询限 2s),应在 handler 内部单独控制,用
context.WithTimeout(ctx, 2*time.Second)并确保所有下游调用都接收这个新 ctx - 拦截器里打印
ctx.Deadline()是调试超时逻辑最有效的手段,比埋点更直接
真正难的不是写一个拦截器,而是搞清它在哪一层起作用、对哪些数据可见、以及它和 handler 共享的 ctx 生命周期边界在哪里。多数线上问题都出在 metadata 传递遗漏、流式拦截器忘记调用 handler、或者 timeout 被多层覆盖导致不可控。










