一次 HTTP(Hypertext Transfer Protocol,超文本传输协议)请求依次查询用户、订单和库存。用户在 200 毫秒后关闭页面,如果下游仍执行 5 秒查询,会继续占用 goroutine、数据库连接和网络资源。Context 把“这次请求已经没有继续价值”沿调用链传播。
没有统一信号
每层自己创建超时,父请求取消后子任务仍运行;资源释放时间不可预测。
传递同一棵 Context
入口建立总预算,下游从父级派生更短预算;任何父级取消都会通知后代。
Context(上下文)是沿函数调用链传递截止时间、取消信号和请求范围值的标准接口。goroutine 是由 Go 运行时调度的轻量并发任务;Context 不会强行杀死它,而是关闭一个信号通道,每段工作必须主动监听并尽快退出。
一次 HTTP(Hypertext Transfer Protocol,超文本传输协议)请求依次查询用户、订单和库存。用户在 200 毫秒后关闭页面,如果下游仍执行 5 秒查询,会继续占用 goroutine、数据库连接和网络资源。Context 把“这次请求已经没有继续价值”沿调用链传播。
每层自己创建超时,父请求取消后子任务仍运行;资源释放时间不可预测。
入口建立总预算,下游从父级派生更短预算;任何父级取消都会通知后代。
| 方法 | 白话解释 | 典型使用 |
|---|---|---|
Deadline() | 返回最晚工作到什么时候 | 下游计算剩余预算 |
Done() | 取消时关闭的只读 Channel | select 中退出等待 |
Err() | 解释为何结束 | 区分 Canceled 与 DeadlineExceeded |
Value(key) | 读取请求链路携带的小型元数据 | 请求 ID、认证主体、追踪信息 |
context.Background() 通常作为主函数、初始化和请求入口的根;context.TODO() 表示调用点还没决定该接哪个 Context,不应被当成永久逃避传播的方案。
WithCancel、WithTimeout 和 WithDeadline 都从父 Context 派生子 Context。root(根)是这棵调用树的入口 Context,child(子节点)是从父级派生出的新 Context。父取消会传到所有后代;取消一个子节点不会反向取消父亲,也不会影响兄弟节点。
WithCancelCause 可以保存取消原因,调用方用 context.Cause 读取;普通 Err 仍返回标准的取消或超时错误。
入口总预算 800ms,解析和鉴权已经用掉 150ms,数据库调用最多只剩 650ms。若数据库层固定再开 2 秒超时,它仍会受父级更早截止时间约束;但硬编码会让意图混乱,也可能在缺失父级预算时拖太久。
ctx, cancel := context.WithTimeout(parent, 800*time.Millisecond) defer cancel() dbCtx, dbCancel := context.WithTimeout(ctx, 300*time.Millisecond) defer dbCancel() row := db.QueryRowContext(dbCtx, query, id)
WithValue 适合跨 API 边界的请求范围数据,例如追踪 ID、认证主体和租户标识。普通业务参数应显式写进函数参数或配置结构体。
type requestIDKey struct{}
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey{}, id)
}
func RequestID(ctx context.Context) (string, bool) {
id, ok := ctx.Value(requestIDKey{}).(string)
return id, ok
}
WithCancel、WithTimeout 和 WithDeadline 返回的 CancelFunc 会解除父级对子级的引用,并停止相关计时器。即使函数提前成功返回,也应调用,通常紧跟 defer cancel()。
| 错误 | 后果 | 修复 |
|---|---|---|
| 忘记 cancel | 子级和计时器保留到父取消或超时 | 创建后立即 defer |
| 循环里 defer cancel | 直到外层函数结束才集中释放 | 抽成单次函数或每轮显式 cancel |
| 工作不监听 Done | 取消信号到达,goroutine 仍不退出 | 阻塞点使用支持 Context 的 API 或 select |
| 把 Context 存进结构体 | 生命周期模糊、复用错误 | 作为第一个参数逐层传递 |
下面的完整程序创建 30 个任务,每个任务模拟工作 100 毫秒,而整批任务只有 2 秒预算。运行后可以观察到:超时会关闭 Done 信号,生产者停止发送,Worker 返回具体取消原因。
package main
import (
"context"
"errors"
"fmt"
"time"
)
type Job struct{ ID int }
func process(ctx context.Context, job Job) error {
select {
case <-ctx.Done():
return context.Cause(ctx)
case <-time.After(100 * time.Millisecond):
fmt.Println("完成任务", job.ID)
return nil
}
}
func worker(ctx context.Context, jobs <-chan Job) error {
for {
select {
case <-ctx.Done():
return context.Cause(ctx)
case job, ok := <-jobs:
if !ok { return nil }
if err := process(ctx, job); err != nil { return err }
}
}
}
func run(parent context.Context) error {
ctx, cancel := context.WithTimeoutCause(
parent, 2*time.Second, errors.New("batch budget exhausted"),
)
defer cancel()
jobs := make(chan Job)
go func() {
defer close(jobs)
for id := 1; id <= 30; id++ {
select {
case <-ctx.Done():
return
case jobs <- Job{ID: id}:
}
}
}()
return worker(ctx, jobs)
}
func main() {
if err := run(context.Background()); err != nil {
fmt.Println("结束原因:", err)
}
}
HTTP 或远程服务请求、数据库查询、批处理、把工作分发给多个执行者的并发处理,以及服务关闭,都适合用 Context 统一传递生命周期。进程级长期对象通常从 Background 派生;需要独立于请求完成的后台任务必须明确重新设计生命周期,不能无意识切断取消。