Go context
context 在调用链上传取消、超时/截止时间与请求范围值;Done/Err、派生树传播,以及勿把 ctx 当结构体字段的惯例与例外。
[!info] 关联笔记
Go context
这个概念为什么会出现
只会 go f() 远远不够。工程里真正难的是:
- 请求已经断开,下游 RPC/SQL 还在跑
- 总时限 2s,子调用还要再分预算
- 同一条调用链上的 goroutine 要一起停
- 需要透传 request-id,但不想污染每个业务函数的领域参数列表到失控
context.Context 成为标准答案:把截止时间、取消信号与少量请求范围元数据,沿着 API 调用链显式向下传。它管的是“这条工作还该不该继续”,不是“业务对象有哪些字段”。
[!abstract] 一句话理解 Context 是可派生的请求范围控制句柄:父取消则子取消;
Done关闭表示退出信号,Err解释原因;值透传应极克制,且 ctx 通常作为函数第一个参数而不是长期塞进结构体。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:下单接口给“扣库存”子调用设 50ms 预算
用户点击购买后,handler 最多愿意等 50ms 做库存预扣。
下游如果更慢(锁竞争、网络抖),不能无限挂着占住请求槽位——
应取消子调用并返回超时,把线程/连接让给别人。
context.WithTimeout 就是“给这条调用链设总预算”;
子函数通过 select 监听 ctx.Done(),超时就停。
package main
import (
"context"
"fmt"
"time"
)
// reserveStock 模拟“扣减库存”的慢操作。
//
// 业务意图:
// - 正常路径约 100ms 完成(演示用 sleep 代替 RPC/SQL);
// - 若请求预算先耗尽,必须立刻停并返回 ctx.Err(),
// 避免超时后还在改库存。
//
// 教学点:
// - 取消是协作式的:函数自己 select Done,运行时不会强杀;
// - 支持 ctx 的标准库/驱动同理,内部会听 Done。
func reserveStock(ctx context.Context, sku string) error {
select {
case <-time.After(100 * time.Millisecond):
// 模拟下游终于成功——但若预算只有 50ms,这条路通常走不到。
fmt.Println("reserved", sku)
return nil
case <-ctx.Done():
// 预算用尽或上级 cancel:停止并上报原因(Canceled / DeadlineExceeded)。
fmt.Println("reserve stopped:", sku, ctx.Err())
return ctx.Err()
}
}
func main() {
// 请求级根:真实 HTTP 里常用 r.Context(),这里用 Background 顶上。
root := context.Background()
// 给本请求的“扣库存”步骤只留 50ms。
ctx, cancel := context.WithTimeout(root, 50*time.Millisecond)
// 即使已经超时,也要 cancel:释放 WithTimeout 创建的 timer 等资源。
defer cancel()
if err := reserveStock(ctx, "sku-42"); err != nil {
// handler 可映射为 504/409 等;这里只展示错误回到 main。
fmt.Println("checkout:", err)
}
}
建议运行:
go run .
期望输出:
reserve stopped: sku-42 context deadline exceeded
checkout: context deadline exceeded
结合场景再看三个关注点
-
超时后
Done关闭,业务必须自己退出
reserveStock不听ctx就会在超时后继续改库存——这是事故,不是“context 没用”。 -
defer cancel()是纪律
成功、失败、提前返回都要释放 timeout 资源。 -
ctx 管“还该不该继续”,不管业务字段
SKU、用户 ID 仍走普通参数;ctx 只传取消/截止与极少量请求元数据。
核心概念与准确模型
接口要点
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
| 方法 | 含义 |
|---|---|
Done | 关闭的 channel 表示应停止;可 select |
Err | Done 关闭后返回 Canceled 或 DeadlineExceeded 等 |
Deadline | 截止时间(若有) |
Value | 按 key 取请求范围值 |
创建与派生
context.Background() // 根:main、init、测试顶层
context.TODO() // 尚不清楚用谁时的占位,应尽快替换
ctx, cancel := context.WithCancel(parent)
ctx, cancel := context.WithTimeout(parent, d)
ctx, cancel := context.WithDeadline(parent, t)
ctx := context.WithValue(parent, key, val)
派生形成树:
- 子 ctx 在父取消、自己 cancel、或自己超时/截止时变为 Done
- 取消向下传播,不会“子取消父”
- 每次
WithCancel/WithTimeout/WithDeadline返回的cancel都应在不用时调用
flowchart TD
BG["Background"] --> REQ["WithTimeout 请求级"]
REQ --> DB["WithValue / 子调用"]
REQ --> RPC["WithCancel 某分支"]
REQ -->|超时或 cancel| DONE["子树 Done"]
Done 与 Err
select {
case <-ctx.Done():
return ctx.Err()
default:
}
context.Canceled:调用了 cancel 或父取消context.DeadlineExceeded:超时或过了 deadline
Err() 在 Done 未关闭前返回 nil。
Value:请求范围元数据
type ctxKey int
const keyReqID ctxKey = 1
ctx = context.WithValue(ctx, keyReqID, "abc")
id, _ := ctx.Value(keyReqID).(string)
约定:
- key 用未导出类型,避免包间碰撞。
- 只放跨中间件/API 边界的横切数据:request id、auth 主体、trace span……
- 不要把业务必需参数(userID 作为核心入参、整颗配置树)塞进 Value 逃避函数签名。
- Value 是不可变链路:每次 WithValue 返回新 ctx。
API 惯例
Go Wiki:Contexts
- 需要时,
ctx作为第一个参数,命名ctx。 - 不传
nil;不知道时用context.TODO()。 - 不把 Context 放在结构体里长期保存——默认惯例。
- 函数若启动 goroutine,应让其能观察 ctx 或文档化生命周期。
“不要存进 struct”的准确 nuance
常见禁令:
type Server struct {
ctx context.Context // 通常错误
}
原因:
- ctx 有生命周期,结构体往往更长
- 容易用错请求级 ctx 去跑后台任务
- 取消语义变得隐式、难测
可接受的例外形态(仍要谨慎):
- 方法签名继续显式收
ctx,结构体只存 独立于单请求的 依赖 - 某个 worker 值仅代表“这一次运行”,构造时注入 ctx,且生命周期与该 run 一致
- 例如:
type job struct { ctx context.Context; ... }仅在该 job 范围内使用
原则:ctx 的寿命 ≥ 使用它的工作寿命,且谁 cancel 一目了然。优先“参数传入”而不是“字段暗藏”。
传播与协作取消
取消不是强制线程杀死:
- 阻塞在支持 ctx 的调用上(
http.NewRequestWithContext、DB driver 等) - 在
select里等ctx.Done() - 循环中轮询
ctx.Err()
若忽略 Done,goroutine 仍会泄漏。见 go-goroutine-channel-context-and-cancellation-relationship。
与 channel 超时的关系
- 本地一次性超时可用
time.After/NewTimer - 跨 API 边界的取消/截止时间应传
context,以便子调用继续派生
请求超时预算常层层 WithTimeout/WithDeadline 收紧,而不是每个子调用重新从“现在”算更长的时间。
设计动机
- 标准化请求生命周期
HTTP/RPC 中间件与业务代码说同一种取消语言。 - 显式优于 TLS 隐式上下文
函数签名可见依赖,便于测试与推理。 - 取消可组合
树状派生自然表达“整请求停”与“子树停”。
边界情况与反直觉行为
- 忘记 cancel
WithTimeout/WithCancel泄漏 timer 与子树资源直到父结束。 - 把 Background 传进每个请求
失去取消与超时。应使用请求 ctx(如r.Context())。 - 在已取消 ctx 上继续重活
必须先检查或让下游立刻返回。 - Value 类型断言失败
静默零值;应用方要明确处理。 - 派生过多 WithValue
链变长、可读性下降;非热点一般可接受,但仍应克制。 - parent 已 Done 再 WithCancel
子 ctx 立即取消。
常见误区
[!warning] 常见误区:context 当参数垃圾袋 错误:
WithValue塞 user、repo、config、db。
正确:横切元数据走 Value;领域依赖走参数或 struct 字段。
[!warning] 常见误区:认为 cancel 会打断任意代码 错误:以为调用 cancel 后函数栈自动停。
正确:必须协作检查 Done 或使用支持 ctx 的阻塞 API。
[!warning] 常见误区:传 nil context 错误:
Do(nil, ...)在下游 panic 或语义不清。
正确:Background/TODO/请求 ctx。
[!warning] 常见误区:结构体存请求 ctx 做后台循环 错误:把第一个 HTTP 请求的 ctx 存进 Server,用于所有后台工作。
正确:Server 用Background或独立生命周期 ctx;每请求用r.Context()。
工程实践
- handler 入口:
ctx := r.Context(),向下传。 - 所有出口
defer cancel()。 - 错误返回 优先
return ctx.Err(),便于上层识别取消。 - 启动 goroutine 前想清楚:用哪个 ctx?谁 Wait?
- 测试 用
WithCancel立即 cancel 断言快速返回。 - 日志/trace 从 ctx Value 取 request id,保持业务函数签名干净。
- 优雅停机 根 cancel 触发,in-flight 请求用各自 ctx 或 Shutdown 语义(go-graceful-shutdown)。
可验证实验
WithTimeout短于工作时间,确认提前返回DeadlineExceeded。- 父 cancel 后子
select立刻返回。 - 子 cancel 不影响父
Err()。 - 忘记
cancel的WithTimeout(对照文档理解资源释放责任)。 WithValue与未导出 key,另一包无法轻易伪造同 key。- 不检查 Done 的死循环在 cancel 后仍运行(反例)。
本节总结
- 本质:调用链上的取消/截止时间/轻量元数据载体。
- 关键 API:WithCancel/Timeout/Deadline/Value、Done、Err。
- 传播:父→子树;协作式退出。
- 纪律:首参 ctx、不传 nil、defer cancel、Value 克制、慎存 struct。
- 下一步:与 channel/select 组合;HTTP 与停机场景落地。
自测题
概念题
WithTimeout返回的cancel为什么仍要调用?- 子 ctx 取消会取消父 ctx 吗?
- 哪些数据适合
WithValue,哪些不适合?
代码推理题
ctx, cancel := context.WithCancel(context.Background())
child, stop := context.WithCancel(ctx)
cancel()
<-child.Done()
fmt.Println(child.Err())
stop()
child.Err() 是什么?stop 是否仍应调用?
工程思考题
服务有“进程级后台同步”和“每 HTTP 请求查询”。两类工作的 ctx 根应如何选?
参考答案
展开
- 释放与该 ctx 关联的资源(如 timer);若工作提前结束,不 cancel 可能拖到截止才回收。
- 不会;取消只向下传播。
- 适合 request id / 认证主体 / trace;不适合业务核心入参与依赖注入容器。
代码:父取消使 child Done,通常context.Canceled;stop仍应调用(幂等释放本层资源)。
工程:后台用服务生命周期 ctx(来自 main/run);HTTP 用r.Context();切勿混用。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| Package context | 标准库 | API |
| Go Blog — Go Concurrency Patterns: Context | 博客 | 设计与用法 |
| Code Review Comments — Contexts | Wiki | 惯例 |
| Spec 无单独章节,语义以包文档为准 | 文档 | Done/Err 行为 |
笔记元信息
- 建议文件名:
go-context.md - 所属阶段:阶段四
- 本篇状态:已深化
- 建议下一篇:取消传播关系