并发编程中,除了"互斥访问"和"等待通知"之外,还有一类核心需求——取消和超时控制。比如:HTTP 请求超时了,下游所有 goroutine 都应该停止工作;用户取消了操作,正在进行的数据库查询应该被中断。Go 标准库的 context 包就是为了解决这类问题而生的。
一、Context 解决什么问题?
假设一个 HTTP 请求触发了多个 goroutine 并行处理:
HTTP 请求
│
├── goroutine: 查询数据库
├── goroutine: 调用下游 API
└── goroutine: 读取缓存如果请求超时了(比如客户端断开连接),这三个 goroutine 应该如何知道"不用干了"?
没有 Context: 有 Context:
请求超时 请求超时
│ │
│ 数据库查询还在跑... │── ctx.Done() 触发
│ 下游 API 还在等... │── 所有 goroutine 收到信号
│ 缓存读取还在等... │── 立即停止,释放资源
│ │
└── 资源浪费 💀 └── 干净退出 ✅Context 提供了三个核心能力:
┌──────────────────────────────────────────────────────────────┐
│ Context 的三大能力 │
├──────────────────────────────────────────────────────────────┤
│ 1. 取消信号传播 父 Context 取消 → 所有子 Context 自动取消 │
│ 2. 超时/截止时间 到期后自动取消 │
│ 3. 传递请求级数据 trace ID、用户信息等跨 API 边界传递 │
└──────────────────────────────────────────────────────────────┘二、Context 的接口
context.Context 是一个接口,只有四个方法:
┌──────────────────────────────────────────────────────────────────────┐
│ context.Context 接口 │
├──────────────────────────────────────────────────────────────────────┤
│ Deadline() (deadline time.Time, ok bool) │
│ 返回截止时间(如果设置了的话) │
│ │
│ Done() <-chan struct{} │
│ 返回一个 channel,Context 被取消时关闭 │
│ │
│ Err() error │
│ Done channel 关闭后,返回取消原因 │
│ • context.Canceled ← 主动取消 │
│ • context.DeadlineExceeded ← 超时 │
│ │
│ Value(key any) any │
│ 获取与 key 关联的值 │
└──────────────────────────────────────────────────────────────────────┘三、创建 Context 的四种方式
// 1. 根 Context(不可取消,通常作为起点)
ctx := context.Background() // 主函数、初始化、测试
ctx := context.TODO() // 不确定用哪个时的占位符
// 2. 可取消的 Context
ctx, cancel := context.WithCancel(parentCtx)
defer cancel() // ✅ 必须调用,释放资源
// 3. 带超时的 Context
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()
// 4. 带截止时间的 Context
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(parentCtx, deadline)
defer cancel()它们形成一棵树状结构:
context.Background()
│
├── WithCancel() ──→ ctx1
│ │
│ ├── WithTimeout(3s) ──→ ctx2
│ │
│ └── WithValue("traceID", "abc") ──→ ctx3
│
└── WithTimeout(10s) ──→ ctx4
│
└── WithCancel() ──→ ctx5
取消 ctx1 → ctx2、ctx3 也被取消
取消 ctx4 → ctx5 也被取消
取消 ctx5 → ctx4 不受影响(子不影响父)关键规则:父 Context 取消,所有子 Context 自动取消。子 Context 取消,不影响父 Context。
四、基本用法
超时控制
func fetchData(ctx context.Context) (string, error) {
// 模拟一个耗时操作
ch := make(chan string, 1)
go func() {
time.Sleep(3 * time.Second) // 模拟耗时
ch <- "data"
}()
select {
case data := <-ch:
return data, nil
case <-ctx.Done():
return "", ctx.Err() // 超时或被取消
}
}
func main() {
// 设置 1 秒超时
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
data, err := fetchData(ctx)
if err != nil {
fmt.Println("失败:", err) // context deadline exceeded
return
}
fmt.Println("成功:", data)
}取消传播
func main() {
ctx, cancel := context.WithCancel(context.Background())
// 启动多个 worker
for i := 0; i < 5; i++ {
go worker(ctx, i)
}
time.Sleep(2 * time.Second)
cancel() // 通知所有 worker 停止
time.Sleep(time.Second) // 等待 worker 退出
}
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d: 收到取消信号,退出\n", id)
return
default:
fmt.Printf("Worker %d: 工作中...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}传递请求级数据
type contextKey string
const traceIDKey contextKey = "traceID"
func withTraceID(ctx context.Context, traceID string) context.Context {
return context.WithValue(ctx, traceIDKey, traceID)
}
func getTraceID(ctx context.Context) string {
if v, ok := ctx.Value(traceIDKey).(string); ok {
return v
}
return "unknown"
}
func handleRequest(ctx context.Context) {
traceID := getTraceID(ctx)
fmt.Printf("[%s] 处理请求\n", traceID)
queryDB(ctx)
}
func queryDB(ctx context.Context) {
traceID := getTraceID(ctx)
fmt.Printf("[%s] 查询数据库\n", traceID)
}五、Context 的传递规范
Go 官方对 Context 的使用有明确的规范:
┌──────────────────────────────────────────────────────────────────┐
│ Context 使用规范 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 1. Context 作为函数的第一个参数,命名为 ctx │
│ func DoSomething(ctx context.Context, arg string) error │
│ │
│ 2. 不要把 Context 放到 struct 中 │
│ (Go 1.7 的建议;某些长生命周期对象可以例外) │
│ │
│ 3. 不要传递 nil Context │
│ 不确定用什么就用 context.TODO() │
│ │
│ 4. WithValue 只传递请求级的数据 │
│ trace ID、请求 ID、认证 token 等 │
│ 不要传递业务参数(函数签名应该明确依赖) │
│ │
│ 5. 同一个 Context 可以传给多个 goroutine │
│ Context 是并发安全的 │
│ │
└──────────────────────────────────────────────────────────────────┘六、实现原理
cancelCtx
WithCancel 返回的核心类型是 cancelCtx:
┌──────────────────────────────────────────────────────────┐
│ cancelCtx │
├──────────────────────────────────────────────────────────┤
│ │
│ Context ← 父 Context │
│ done chan struct{} ← Done() 返回这个 channel │
│ children map[canceler]struct{} ← 子 Context 列表 │
│ err error ← 取消原因 │
│ │
│ cancel(): │
│ 1. 关闭 done channel │
│ 2. 遍历 children,逐个取消 │
│ 3. 从父 Context 中移除自己 │
│ │
└──────────────────────────────────────────────────────────┘timerCtx
WithTimeout 和 WithDeadline 返回的是 timerCtx,它在 cancelCtx 基础上加了定时器:
┌──────────────────────────────────────────────────────────┐
│ timerCtx │
├──────────────────────────────────────────────────────────┤
│ │
│ cancelCtx ← 内嵌 cancelCtx │
│ timer *time.Timer ← 到期后自动触发 cancel │
│ deadline time.Time ← 截止时间 │
│ │
│ 到期时: │
│ timer 触发 → 调用 cancelCtx.cancel() │
│ → 关闭 done channel │
│ → 取消所有子 Context │
│ │
└──────────────────────────────────────────────────────────┘valueCtx
WithValue 返回的是 valueCtx,它的查找是链式的:
ctx3 := WithValue(ctx2, "k3", "v3")
ctx2 := WithValue(ctx1, "k2", "v2")
ctx1 := WithValue(bg, "k1", "v1")
bg := Background()
ctx3.Value("k1") 的查找路径:
ctx3 → key 不匹配
ctx2 → key 不匹配
ctx1 → key 匹配 ✅ 返回 "v1"注意:Value 的查找是 O(n) 的,n 是 WithValue 的嵌套层数。不要存大量数据,也不要在性能敏感路径上频繁调用。
七、使用 Context 最常踩的 4 个坑
坑 1:忘记调用 cancel
WithCancel/WithTimeout/WithDeadline 返回的 cancel 函数 必须调用,否则会导致资源泄漏:
// ❌ 忘记调用 cancel,导致 goroutine 和 timer 泄漏
func handle() {
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
// cancel 被丢弃了 💀
doWork(ctx)
}// ✅ 始终 defer cancel
func handle() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // 即使操作提前完成,也释放 timer 资源
doWork(ctx)
}小技巧:即使 Context 会因超时自动取消,也应该 defer cancel()。因为 cancel 会立即释放内部 timer 和子 Context 的关联,不必等到超时。
坑 2:WithValue 的 key 用了内置类型
// ❌ 用 string 做 key,可能和其他包冲突
ctx = context.WithValue(ctx, "userID", 123)// ✅ 用自定义未导出类型做 key,避免冲突
type contextKey string
const userIDKey contextKey = "userID"
ctx = context.WithValue(ctx, userIDKey, 123)坑 3:把业务参数塞进 Context
// ❌ 把业务参数放进 Context
ctx = context.WithValue(ctx, "orderID", orderID)
processOrder(ctx) // 函数签名看不出需要 orderID
// ✅ 业务参数用函数参数传递
processOrder(ctx, orderID) // 依赖关系一目了然WithValue 只适合传递请求级的横切关注点(trace ID、认证信息、请求 ID),不适合传递业务数据。
坑 4:不检查 ctx.Done()
启动的 goroutine 如果不检查 ctx.Done(),取消信号就传不进去:
// ❌ 不检查 Done,cancel 调了也没用
func doWork(ctx context.Context) {
for i := 0; i < 1000000; i++ {
heavyComputation(i) // 死干,不看取消信号
}
}// ✅ 在循环中定期检查
func doWork(ctx context.Context) {
for i := 0; i < 1000000; i++ {
select {
case <-ctx.Done():
return // 收到取消信号,退出
default:
}
heavyComputation(i)
}
}八、Go 1.21+ 新增 API
Go 1.21 新增了几个实用的 Context 函数:
// WithoutCancel:创建一个不会被父 Context 取消的子 Context
// 适合在父请求结束后还需要继续的后台任务
ctx := context.WithoutCancel(parentCtx)
// AfterFunc:Context 取消时执行回调
stop := context.AfterFunc(ctx, func() {
fmt.Println("ctx 被取消了,执行清理")
})
// stop() 可以阻止回调执行// WithCancelCause(Go 1.20+):取消时附带原因
ctx, cancel := context.WithCancelCause(parentCtx)
cancel(fmt.Errorf("用户主动取消")) // 附带原因
// 获取取消原因
err := context.Cause(ctx) // "用户主动取消"九、实战建议
- 所有外部调用都传 Context——HTTP 请求、数据库查询、RPC 调用,都应该接受 Context
- 始终
defer cancel()——防止资源泄漏,即使 Context 会自动超时 - WithValue 只传请求级数据——trace ID、认证信息,不传业务参数
- 长时间操作要检查
ctx.Done()——否则取消信号传不进去 - 超时要合理设置——下游超时应该小于上游超时,留出处理和返回的时间
// 超时层级示例
// HTTP handler: 10s
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
// 数据库查询: 3s(小于 handler 的 10s)
dbCtx, dbCancel := context.WithTimeout(ctx, 3*time.Second)
defer dbCancel()# 检查 Context 泄漏
go vet ./...
go test -race ./...Context 是 Go 并发编程的基础设施——它不是一个同步原语,而是一个 信号传播机制。理解它的树状取消传播和 Value 的链式查找,遵循官方的使用规范,你就能在并发程序中优雅地处理超时和取消。