gRPC Metadata是类比HTTP Header的键值对元数据容器,专用于RPC调用中传递鉴权令牌、追踪ID等控制面信息;它底层序列化进gRPC二进制帧头而非HTTP/2 headers,故不能直接用http.Header操作,必须通过metadata.Pairs构造并用AppendToOutgoingContext注入、FromIncomingContext提取,且key须小写、value为[]string。

gRPC Metadata 是什么,为什么不能直接用 HTTP Header
Go 的 gRPC 默认不透传 HTTP Header,而是用 metadata.MD 作为独立的元数据容器——它底层被序列化进 gRPC 的二进制帧头(不是 HTTP/2 的 :authority 或 custom headers),所以服务端收不到你用 http.Header 手动塞的字段。想传自定义键值对(比如 user-id、trace-id),必须走 metadata.Pairs + grpc.Header / grpc.Trailer 流程。
客户端怎么发:用 metadata.AppendToOutgoingContext 写入
不能直接改 context.Context 的底层结构,必须用 gRPC 提供的包装函数。常见错误是忘记调用 AppendToOutgoingContext,或误用 metadata.New 覆盖而非追加。
-
metadata.Pairs("user-id", "123", "trace-id", "abc456")是标准写法,key 必须小写(gRPC 规范要求) - 必须在调用 RPC 方法前注入:
ctx = metadata.AppendToOutgoingContext(ctx, "user-id", "123") - 如果要传多个字段,一次调用即可,不用多次
AppendToOutgoingContext(它支持变参) - 注意:
metadata.Pairs返回的是metadata.MD,但客户端侧只需传 key-value 字符串对,无需手动转MD
ctx := context.Background()
ctx = metadata.AppendToOutgoingContext(ctx, "user-id", "123", "trace-id", "abc456")
resp, err := client.SayHello(ctx, &pb.HelloRequest{Name: "Alice"})
服务端怎么收:用 grpc.Peer 和 metadata.FromIncomingContext
服务端收到的 metadata 不会自动挂到 http.Request.Header 上,必须显式从 context 解包。容易漏掉的是没检查返回值是否为 true,导致 panic 或读到空 map。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 调用
metadata.FromIncomingContext(ctx),它返回(MD, bool),第二个值为false表示没收到任何 metadata - key 是小写的,比如
md["user-id"],不是md["User-ID"]或md["USER-ID"] - 值是
[]string类型,即使只传一个,也要取[0],例如md["user-id"][0] - 如果需要获取 peer 信息(如客户端 IP),得额外用
peer.FromContext(ctx),和 metadata 无关
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.InvalidArgument, "missing metadata")
}
userID := md["user-id"]
if len(userID) == 0 {
return nil, status.Error(codes.InvalidArgument, "missing user-id")
}
// ...
}
Metadata 的生命周期和常见陷阱
Metadata 只在单次 RPC 调用中有效,不会跨流、不持久、不继承子 goroutine context。最常踩的坑是:在中间件里修改了 context 但没传给后续 handler;或者用 context.WithValue 试图“模拟” metadata —— 完全无效。
- Unary RPC:metadata 随请求一次性发送,服务端只能在 handler 入口读一次
- Streaming RPC:客户端可在
Send前反复调用AppendToOutgoingContext,但服务端每次Recv收到的 metadata 是初始那次的,后续无法更新 - Header vs Trailer:只有
grpc.Header(服务端用sendHeader)能被客户端通过metadata.FromIncomingContext读到;grpc.Trailer是响应结束时发的,客户端要用Trailer()方法单独取 - 敏感信息别硬编码进 metadata:比如 token、密码——它默认不加密,走明文 base64 编码,需配合 TLS 或自定义 credentials
真正麻烦的地方在于:metadata 看似简单,但一旦混用 HTTP 中间件、gRPC-gateway、或自定义 dialer,就很容易在某一层被丢弃或大小写转换。建议所有关键字段在客户端发之前先打日志,服务端收完立刻校验存在性——别等业务逻辑跑一半才发现 user-id 是空的。










