golang context
写并发程序时,经常会遇到同一个问题:发起方已经不需要结果了,后台任务该怎样尽快停下来?
例如,一个 HTTP 请求会调用数据库和远程服务;如果客户端已经断开连接,继续执行这些工作通常只会浪费连接、CPU 和 goroutine。Go 的 context 就是用来把“这个请求还要不要继续”“最晚什么时候结束”“调用链上有哪些请求元数据”统一传下去的机制。
可以先把它理解为一张随调用链向下传递的“请求说明书”:
- 请求取消了,说明书会通知下游任务收尾;
- 到了超时时间,说明书会自动发出取消通知;
- 需要传递少量请求范围的信息时,下游可以从说明书中读取。
它不会强制杀掉 goroutine。context 只负责发通知;收到通知后是否退出,仍要由代码主动处理。
context 能解决什么问题
context 最常用于下面三类事情:
| 场景 | 作用 | 常见例子 |
|---|---|---|
| 取消 | 告诉下游“不必继续了” | 客户端断开、上游提前返回 |
| 超时或截止时间 | 为一次操作设定最长执行时间 | 数据库查询最多 2 秒 |
| 请求范围的数据 | 在调用链中传递少量元数据 | trace ID、认证身份 |
前两类是 context 最重要的职责。WithValue 虽然也常用,但它不适合代替函数参数或配置对象。
先认识 Context 接口
context.Context 是一个只读接口,核心方法不多:
1 | |
可以这样记:
Done():一个通知通道。Context 被取消或超时时,这个通道会被关闭。Err():告诉你结束原因,通常是context.Canceled或context.DeadlineExceeded。Deadline():获取最晚结束时间;没有设置时,ok为false。Value():读取沿调用链传递的请求范围数据。
实际代码中最常用的是 Done():把它放进 select,就能在等待工作结果的同时响应取消。
Context 是一棵向下传播的树
Context 通常从一个父 Context 派生出子 Context。父节点结束时,所有后代都会收到通知;但取消一个子节点,不会反过来取消父节点或兄弟节点。
这正好契合调用链:一个请求作为父节点,数据库查询、RPC 调用和并发子任务都使用它派生出的子节点。Value 的查找也沿父链向上进行,因此子节点能读取父节点保存的请求元数据。
从哪里创建 Context
常用构造方式如下:
| 函数 | 什么时候用 |
|---|---|
context.Background() | 程序入口、初始化、测试等没有父请求的地方 |
context.TODO() | 暂时不确定该传哪个 Context 时的占位符,应尽快替换 |
context.WithCancel(parent) | 需要主动结束一组下游工作 |
context.WithTimeout(parent, timeout) | 最常用,为操作设置最长执行时间 |
context.WithDeadline(parent, deadline) | 已经有明确的结束时刻 |
context.WithValue(parent, key, value) | 传递请求范围数据 |
在业务调用链中,不要反复创建 Background();优先接收并继续传递上游给你的 ctx。只有确实需要为局部工作增加取消或超时策略时,才从它派生子 Context。
最小示例:手动取消 goroutine
下面的 goroutine 一直等待取消通知。调用 cancel() 后,Done() 通道关闭,goroutine 得以退出。
1 | |
这里有两个细节:
cancel可以安全地多次调用,因此通常紧跟defer cancel()。- 取消不是中断指令。只有 goroutine 像上例一样监听
ctx.Done(),它才会退出。
超时:在“完成”和“放弃”之间二选一
实际服务更常设置超时。下面的操作需要 3 秒,但 Context 只允许 2 秒,因此会走超时分支:
1 | |
即使操作提前完成,也应保留 defer cancel():它能及时释放计时器和派生 Context 关联的资源。WithDeadline 的行为相同,只是它接收一个具体时间点,而不是一段时长。
在请求链路中正确传递
Web 服务中,r.Context() 已经包含客户端连接的生命周期。好的做法是把它作为每层函数的第一个参数,一路传到数据库、RPC 和并发任务:
1 | |
如果自行启动 goroutine,循环、发送结果和等待结果的每一端也都要考虑 ctx.Done()。否则即使 Handler 已经因超时返回,后台 goroutine 仍可能卡在发送或接收操作上。
1 | |
WithValue:只放请求范围的元数据
WithValue 适合传递 trace ID、认证身份等“请求经过很多层都可能需要,但又不属于某个业务函数核心输入”的信息。Key 应使用自定义的私有类型,避免不同包的字符串 Key 意外冲突:
1 | |
不要用它传数据库连接、可选配置、业务入参或大量数据。这些内容应该显式写在函数参数或结构体中,调用关系才清晰,也更容易测试。
常见错误与约定
| 建议 | 原因 |
|---|---|
把 ctx 放在函数第一个参数 | 调用者能立刻看到该操作受取消和超时控制 |
不要传 nil Context | 不需要取消时使用 context.Background() |
| 不要把 Context 存进结构体 | Context 属于一次调用,而不是长期对象的状态 |
创建后总是调用 cancel | 即使自然超时,也能更早释放资源 |
不要用 WithValue 传业务参数 | 显式参数更安全、可读、可发现 |
让阻塞点响应 ctx.Done() | 否则取消只能发出,无法让任务真正离场 |
标准库中已经有许多支持 Context 的 API:http.NewRequestWithContext、sql.DB.QueryContext、exec.CommandContext,以及大量数据库、RPC 客户端的方法。优先使用这些带 Context 的版本,而不是只在最外层设置超时。
总结
context 的重点不在“存数据”,而在管理一次请求及其派生工作的生命周期:
- 从入口拿到
ctx,并一路向下传递。 - 需要局部超时或取消策略时,用
WithCancel、WithTimeout或WithDeadline派生子 Context。 - 在 goroutine、通道、I/O 等可能阻塞的地方主动处理
ctx.Done()。 - 用
WithValue传递少量请求元数据,不把它当成参数包。
掌握这四点,就能让请求结束时的下游工作有序收尾,避免无意义的执行和 goroutine 泄漏。






