grpc拦截器未生效是因注册时机错误,必须在grpc.newserver()时通过grpc.unaryinterceptor和grpc.streaminterceptor选项传入,不能运行时补加;需通过info.fullmethod获取路径,metadata.fromincomingcontext取元数据,避免defer统计耗时,注意空值判断和panic处理。

gRPC拦截器为什么没生效?检查 UnaryInterceptor 和 StreamInterceptor 的注册位置
拦截器不触发,大概率是注册时机或方式错了。gRPC 的拦截器必须在创建 grpc.Server 时通过选项传入,不能在服务注册之后补加。
- 错误写法:
server := grpc.NewServer()后再调用server.SetUnaryInterceptor(...)—— 这个方法根本不存在,Go 的grpc.Server没有运行时修改拦截器的接口 - 正确写法:拦截器必须作为
grpc.ServerOption传给grpc.NewServer(),例如grpc.UnaryInterceptor(myUnary) - 流式拦截器同理,要用
grpc.StreamInterceptor(myStream),且两者互不影响,需同时显式声明才能覆盖两类请求 - 注意:如果用了
grpc.Creds或其他中间件式选项(如grpc.KeepaliveParams),拦截器选项顺序无关,但缺一不可
如何在拦截器里拿到真实请求路径和元数据?别直接读 ctx 而不解析 info
很多同学在 UnaryServerInterceptor 函数里只盯着 ctx,却忘了第一个参数 info *grpc.UnaryServerInfo 才存着关键路由信息。
-
info.FullMethod是完整路径,形如"/helloworld.Greeter/SayHello",可用于路由级日志或限流判断 - 元数据(
metadata.MD)得从ctx里取:md, ok := metadata.FromIncomingContext(ctx);ok为false表示没带 header,不是报错,是常态 - 常见坑:用
metadata.FromOutgoingContext取入参元数据——这是错的,出参上下文在响应阶段才存在 - 如果需要透传或改写元数据,必须用
metadata.AppendToOutgoing在 handler 返回前操作,否则下游收不到
全链路监控要打点,但 defer 记录耗时为什么总不准?
因为 gRPC 拦截器的 defer 很容易被 panic 拦截或提前返回绕过,尤其在 handler 内部 panic 且没被 recover 时,defer 根本不执行。
- 安全做法:把耗时统计逻辑放在拦截器函数末尾,显式计算
time.Since(start)并上报,而非依赖defer - panic 场景下,可用
recover()捕获并强制记录失败指标,但注意不要吞掉原始 panic(除非你明确要降级) - 避免在拦截器里做重 IO(如直连 Prometheus Pushgateway),会拖慢所有请求;建议异步发到本地 channel,由后台 goroutine 批量上报
- 如果用了 opentelemetry-go,优先走
otelgrpc.UnaryServerInterceptor官方封装,它已处理了 context cancel、error 分类、span 生命周期等边界情况
为什么加了拦截器后服务启动就 panic:interface conversion: interface {} is nil?
这个错误通常出现在自定义拦截器里对 info 或 md 做了非空断言但没判空,比如 md["x-request-id"][0] 直接取索引。
立即学习“go语言免费学习笔记(深入)”;
-
info不可能为 nil,但md可能是nil,且即使非 nil,key 也可能不存在或值为空切片 - 安全取值写法:
if ids, ok := md["x-request-id"]; ok && len(ids) > 0 { reqID = ids[0] } - 另一个常见来源:在拦截器里调用
grpc.SendHeader(ctx, ...)或grpc.SetTrailer,但此时 stream 已关闭或还没建立,gRPC 会 panic 而非返回 error - 更隐蔽的坑:拦截器返回了
nilerror,但下游 handler 返回了 non-nil error,而你的监控逻辑假设只有拦截器能出错——这会导致错误分类失真
链路监控不是加个拦截器就完事,真正难的是在不侵入业务的前提下,把超时、取消、网络抖动、序列化失败这些底层信号准确归因到具体 span 上。这些细节藏在 error 类型判断、context 状态检查、以及每次 SendHeader 前的 ctx.Err() != nil 验证里。










