context.WithValue 的键必须是自定义类型(如 type ctxKey string),不可用字符串字面量;HTTP 中须用 r.Context() 而非 context.Background();跨服务需透传字段而非整个 context;valueCtx 仅用于不可变请求元数据,禁存大对象、可变状态或业务实体。

context.WithValue 的键必须是自定义类型,不能用字符串
直接用 "user_id" 或 "trace_id" 这类字符串当 key,看似简单,实际极易引发键冲突或拼写错误——不同包里一不小心定义了同名字符串 key,值就会被覆盖,且编译期完全无法发现。
正确做法是定义未导出的私有类型作为 key:
-
type ctxKey string(注意:必须是新类型,不是别名) const userIDKey ctxKey = "user_id"- 使用时:
ctx = context.WithValue(ctx, userIDKey, 123) - 取值时:
uid := ctx.Value(userIDKey).(int),类型断言安全可编译检查
这样既避免全局污染,又获得类型系统保护。rpcx、gin 中间件等成熟项目都遵循此约定。
HTTP 服务中必须从 r.Context() 拿上下文,不能新建
net/http 的 handler 函数签名不带 context.Context 参数,但每个 *http.Request 都自带一个绑定好的上下文:r.Context()。它已携带客户端连接状态、超时、取消信号等关键信息。
常见错误包括:
- 写
ctx := context.Background()然后一路传下去 → 超时/取消信号丢失,goroutine 泄漏 - 中间件里改了
ctx却没调用req = req.WithContext(newCtx)→ 下游 handler 仍用原始上下文 - 在异步 goroutine 里直接用原始
ctx→ 请求结束但 goroutine 还在跑
正确链路是:ctx := r.Context() → ctx = context.WithValue(ctx, key, val) → req = req.WithContext(ctx) → next.ServeHTTP(w, req)。
跨服务传递时,context.Context 本身不跨网络,只传字段
rpcx、gRPC 或 HTTP 调用时,context.Context 对象无法序列化传输。所谓“上下文透传”,本质是提取关键字段(如 trace_id、request_id),通过载体(HTTP Header / gRPC Metadata)发送,下游再重建新上下文。
具体操作差异:
- HTTP:用
req.Header.Set("X-Trace-ID", traceID),服务端读req.Header.Get("X-Trace-ID"),再context.WithValue(...) - gRPC:用
metadata.Pairs("x-trace-id", traceID)构造md,再metadata.NewOutgoingContext(ctx, md) - 绝对不要把整个
context.Context塞进 JSON body 或 protobuf 字段里 —— deadline、Done channel 等语义全失效
字段命名建议小写加连字符(如 x-trace-id),符合 HTTP 规范,也避免大小写敏感问题。
valueCtx 不是通用存储,只存请求级元数据
context.WithValue 的设计目标很窄:仅用于传递**不可变的、请求生命周期内有效的元数据**,比如 trace_id、user_id、timezone。它不是替代参数或依赖注入的工具。
滥用后果明显:
- 存大对象(如
*sql.DB、map[string]interface{})→ 内存泄漏、GC 压力上升 - 存可变状态(如计数器、缓存 map)→ 多 goroutine 并发读写竞态,无同步保障
- 存业务实体(如
*Order)→ 违反分层职责,耦合中间件与业务逻辑
性能上,valueCtx.Value() 是线性查找,键越多越慢;实测单次调用约 100ns,若在 hot path 循环里频繁调用,应改用显式参数或局部变量缓存。

















