context 不该传业务字段如订单 ID 或用户邮箱,只应承载请求级元数据和生命周期信号;正确做法是用自定义空结构体作 key,HTTP 调用需手动透传超时 header 并配 Transport。

Context 不是用来替代函数参数的,它只该承载请求级元数据和生命周期信号。把业务字段塞进 context.WithValue,迟早掉坑里。
为什么 context.Value 不能传订单 ID 或用户邮箱
context.WithValue 的设计目标是传递不可变的、跨 API 边界的请求元数据(比如 traceID、userID),不是为了绕过函数签名设计。一旦你用它传 orderID 或 user.Email,就会触发两个实际问题:
- 调用方无法从函数签名看出依赖哪些业务数据,可读性和可测试性直线下降
- 下游函数若需修改该值(比如更新邮箱),必须返回新值并靠上层重新传入——而
context是不可变的,改不了 - 类型安全丢失:用
string做 key 时,ctx.Value("user_email")和ctx.Value("user_email2")编译期完全不报错
自定义 key 类型怎么写才不算错
别用 string 当 key。正确做法是定义空结构体类型,哪怕只用一次:
type userKey struct{}
然后这样存取:
立即学习“go语言免费学习笔记(深入)”;
ctx = context.WithValue(ctx, userKey{}, user)
u := ctx.Value(userKey{}).(User)
好处很实在:
- 编译器能检查类型匹配,
userKey{}写错就报错 - 不同模块定义各自的
userKey不会冲突(哪怕名字一样,类型也不同) - IDE 能跳转到定义,比搜字符串靠谱得多
HTTP handler 到 DB 查询之间怎么传 context
关键不是“怎么传”,而是“谁来派生”。常见错误是 handler 里直接用 context.Background() 创建子 context,这会让超时和取消信号断掉。
正确链路是:
- handler 接收的
ctx来自http.Request.Context(),它已经绑定了连接生命周期 - 调用 DB 时,用
db.QueryContext(ctx, ...),而不是db.Query(...) - 如果 DB 操作需要额外超时(比如最多等 2 秒),用
context.WithTimeout(ctx, 2*time.Second)派生,再传给QueryContext - 绝对不要在 handler 里缓存
ctx到 struct 字段,goroutine 泄漏就从这儿开始
跨 HTTP 服务调用时 context 超时怎么生效
context.WithTimeout 在发起 HTTP 请求时,只对当前 goroutine 生效。它不会自动变成 Request.Header 发出去,下游根本收不到。
要让超时穿透到下游服务,必须手动编码:
- 上游:用
req.Header.Set("X-Request-Timeout", strconv.FormatInt(timeoutMs, 10)) - 下游:解析 header,用
context.WithTimeout(parentCtx, timeout)新建 context - 同时,
http.NewRequestWithContext必须调用,否则网络层压根不感知这个 context - Transport 层还得配
DialContext、TLSHandshakeTimeout等,不然 TCP 卡住时 context 超时形同虚设
最易忽略的一点:下游没读 header 时,不能 fallback 到父 context 的 deadline——因为父 context 可能是 30 秒,而你只想让它活 3 秒,必须设默认值。


















