Appearance
context 的超时、取消和传播边界
先说结论
context的核心价值不是“传参数”,而是传递取消信号、截止时间和最小上下文- 一次请求链路里的 goroutine、数据库调用、RPC 调用最好共用一条可取消链路
context.Value只适合放很少量、请求级的元信息,不适合当万能参数包
一、context 到底解决什么问题
Go 里一旦并发多起来,最容易出的问题不是“怎么启动 goroutine”,而是:
- 请求都结束了,后台 goroutine 还在跑
- 上游已经超时了,下游还在继续打数据库
- 多层调用里超时和取消没统一
context 解决的就是这三件事:
- 取消传播
- 超时截止
- 请求级上下文透传
二、超时和取消分别是什么
1. 超时
超时更像“这件事最多做多久”。
常见于:
- HTTP 请求
- 数据库查询
- 下游 RPC
2. 取消
取消更像“上游已经不要结果了,后面一起停”。
比如:
- 用户主动断开请求
- 网关已经超时返回
- 上级任务提前结束
这两者经常一起出现,但语义不完全一样。
三、一个更稳的使用顺序
更推荐按这条链路理解:
- 请求入口生成根 context。
- 往下游调用继续传递这条 context。
- 真正可能阻塞的操作都监听
Done()。 - 用
defer cancel()及时释放资源。
这条链路的关键是:
不要只在入口设超时,却让中间层和下游完全感知不到。
四、最常见的几个坑
1. 把 context 存到 struct 里长期复用
context 是请求级对象,不适合当服务级状态保存。
2. 把业务参数全塞进 context.Value
比如用户对象、订单详情、复杂配置都往里丢,后面阅读和维护都会很差。
3. 创建了超时 context 却不 cancel
这会让定时器和资源比预期活得更久。
4. 只传 context,不真正响应取消
函数签名里有 ctx context.Context,但内部根本不检查 Done(),那就只是“形式上透传”。
五、哪些信息适合放进 context
更适合放的通常是:
- traceId
- requestId
- 用户 ID 这类轻量身份信息
- 语言、租户这类请求级元信息
不适合放的通常是:
- 大对象
- 可变业务状态
- 配置集合
- 当次流程里到处共享的临时缓存
六、排查超时泄漏时先看什么
如果你怀疑 goroutine 没有随着请求一起退出,优先看:
- 入口是否真的设置了超时或取消。
- 下游函数是否持续透传了同一条 context。
- 阻塞点是否监听了
Done()。 - 数据库 / HTTP 客户端是否真正支持 context 取消。
很多“Go 服务 goroutine 越跑越多”的问题,本质上不是 goroutine 本身,而是取消链路断了。
总结
Go 里的 context 最重要的不是语法,而是边界:它负责取消、超时和请求级上下文,不负责承载复杂业务数据。只要把这条边界守住,很多超时、泄漏和下游悬挂问题都会少很多。