HTTP Header 是跨服务传递 context 的唯一可靠载体,因其轻量、不污染 body、全链路支持且调试可见;gRPC 必须用 metadata.MD 而非裸 Header;context.WithValue 的 key 必须是私有类型,Handler 中必须用 r.Context() 而非 context.Background()。

HTTP Header 是跨服务传递 context 的唯一可靠载体
微服务间无法直接传递 context.Context,它只在单进程内有效。所有跨网络的上下文透传,必须靠序列化字段承载——而 HTTP Header 是事实标准,不是“可选方案”。它轻量、不污染业务 body、所有中间件和网关都原生支持读写,调试时 curl -v 一眼可见。
常见错误是把整个 context.Context 序列化进 JSON body 或 gRPC payload,这会导致 Done() 通道丢失、超时信号失效、取消不可传播。正确做法是只提取关键字段:
-
X-Request-ID、X-Trace-ID、X-User-ID这类字符串值必须透传 - 敏感信息(如 token 原文、密码)绝不能放 Header,应走 auth header 或加密凭证
- 大体积数据(如用户完整 profile)禁止塞入 Header,Header 总大小建议控制在 8KB 内
gRPC 中必须用 metadata.MD 而非裸 Header
gRPC 不走 HTTP/1.1,所以不能依赖通用 Header 机制。它提供专用的 metadata.MD,本质是键值对 map,但比字符串 Header 更结构化、支持大小写归一化、自动处理重复键。
典型误用是直接 md.Set("x-user-id", userID),这会忽略大小写规则(gRPC 规范要求 key 全小写),且覆盖已有同名键。正确方式是:
立即学习“go语言免费学习笔记(深入)”;
- 用
metadata.Pairs("x-request-id", reqID, "x-user-id", userID)构造初始 metadata - 服务端通过
metadata.FromIncomingContext(ctx)获取入参 metadata - 客户端用
metadata.NewOutgoingContext(ctx, md)注入 outgoing metadata - 透传链路 A→B→C 时,B 必须显式读取、合并、再传给 C,gRPC 不自动接力
context.WithValue 的 key 必须是私有类型
用字符串当 key 是 Go context 最常见的内存泄漏和运行时 panic 来源。比如 ctx.Value("user_id") 和 ctx.Value("userID") 看似一样,实则完全不同;多个包用相同字符串 key 还会互相覆盖。
必须定义未导出类型作 key:
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
type ctxKey string const userKey ctxKey = "user_id"
然后统一用 context.WithValue(ctx, userKey, userID) 存、ctx.Value(userKey) 取。这样既避免拼写错误,又防止跨包污染。
注意:WithValue 只能存轻量、只读、请求生命周期内不变的数据。不要塞 struct 指针、channel、DB 连接——这些该由依赖注入容器管理,不是 context 的职责。
Handler 中永远用 r.Context(),别碰 context.Background()
HTTP handler 入口第一件事就是调用 r.Context() 获取绑定在请求上的上下文。硬写 context.Background() 或 context.TODO() 会让超时、取消、trace ID 全部失效,上游反向代理中断连接后,下游 goroutine 仍在跑。
中间件增强 context 后,必须用 r.WithContext(newCtx) 替换 request 对象,否则下游 handler 拿到的仍是原始 ctx:
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("X-User-ID")
ctx := context.WithValue(r.Context(), userKey{}, userID)
next.ServeHTTP(w, r.WithContext(ctx)) // ✅ 关键:替换 r
})
}
派生新 context(如加 timeout)时,defer cancel() 是强制动作,漏掉就会 timer 泄露、goroutine 持续堆积。
真正容易被忽略的点在于:context 本身不跨服务,也不跨 goroutine 生命周期。你往里面塞的每个值、设的每个 deadline,都只对当前请求树有效;一旦离开这个请求作用域,它就彻底消失了——这不是缺陷,是设计使然。滥用它去替代配置管理、状态缓存或 session 存储,只会让系统越来越难 debug。

















