Context 不自动传播,必须每层显式接收、派生(如WithTimeout)并传入下一层;漏传则取消/超时失效;ctx须为函数首参;WithValue的key须用未导出空结构体,业务参数应走函数参数而非Value。

Context 多层调用不是“自动穿透”,而是每层都必须显式接收、可能派生、再显式传给下一层——漏传一次,链就断了。
ctx 必须作为第一个参数显式传入每个函数
Go 的 context.Context 不会隐式传播。它只是一个接口值,和 string 或 int 一样,不传就不会有。
- 错误写法:
func doDBWork() error—— 完全没声明ctx参数,下游无法响应取消或超时 - 正确写法:
func doDBWork(ctx context.Context) error,且调用处必须写doDBWork(ctx),不能漏掉变量名 - 如果中间某层做了派生(比如加了超时),必须把新
ctx传下去,原ctx就失效了 - HTTP handler 中拿到的
r.Context()是只读副本;若想让后续中间件看到新值,必须显式调用r = r.WithContext(newCtx)
WithValue 的 key 必须用未导出空结构体,不能用 string
用 "user_id" 这类字符串当 key,是跨包冲突和静默覆盖的根源。
- 定义方式:
type userIDKey struct{}(小写开头,不导出) - 存值:
ctx = context.WithValue(ctx, userIDKey{}, 123) - 取值:
id := ctx.Value(userIDKey{}).(int)—— 编译期可检错,IDE 可跳转,不同包同名 key 也不冲突 - 别传
[]byte、map、func等不可比较类型,运行时 panic - 业务字段如订单 ID、邮箱地址,不该塞进
WithValue;它们应走函数参数,否则签名失真、测试困难
WithTimeout/WithCancel 后必须 defer cancel(),否则 goroutine 泄漏
context.WithTimeout 返回的 cancel 函数不只是“取消用”,它还负责清理内部 timer 和 channel。不调用,资源就卡在那儿。
立即学习“go语言免费学习笔记(深入)”;
- 常见疏漏:在函数里调用
ctx, cancel := context.WithTimeout(...),但忘了defer cancel() - 后果:哪怕函数返回了,
*time.Timer仍活着,goroutine 持续等待直到超时触发,内存和 goroutine 数缓慢上涨 -
WithCancel的cancel()调用后再次调用会 panic;WithTimeout的则幂等,但依然要调 - 测试时容易忽略:mock 场景下没真正跑 timer,泄漏不暴露,上线后才爆发
跨 HTTP 服务调用时,context 超时不自动透传
ctx 里的超时只对当前进程内 goroutine 生效。发 HTTP 请求时,它不会自动变成 X-Request-Timeout header 发出去。
- 下游服务根本收不到你的
WithTimeout信号,除非你手动编码透传 - 正确做法:从
ctx.Deadline()算出剩余时间,设为req.Header.Set("X-Timeout-Ms", ...),下游解析并创建自己的context.WithDeadline - 标准库
http.Client本身支持client.Timeout,但它只控制本机连接/读写,不参与链路级超时协同 - gRPC 用户注意:
grpc.CallOption如grpc.WaitForReady或grpc.MaxCallRecvMsgSize都不替代 context 传递;仍需用grpc.Invoke(ctx, ...)显式传参
最常被绕开的点是:派生新 context 后,忘记把它传给下一层函数,或者误以为 WithValue 修改了原 context。实际它返回新值,旧值不变——这个不可变性既是安全保证,也是出错源头。


















