context 包基础
本教程共 80 篇 · 第 65 篇 · 更新于 2026-07-27 · 约 8 分钟阅读
65. context 包基础
本节目标:搞懂
context.Context接口,学会用WithCancel、WithTimeout、WithValue控制 goroutine,理解 Background 和 TODO 的区别。
context 解决什么问题
goroutine 启动容易,怎么优雅地叫停?比如一个 HTTP 请求触发了 5 个子任务,客户端断开了,这 5 个任务都该停。context 就是干这个的。
它像一根「传递取消信号」的线,从父 goroutine 牵到子 goroutine,父取消时所有子跟着取消。
Context 接口
context.Context 是个接口,四个方法:
type Context interface {
Deadline() (deadline time.Time, ok bool) // 截止时间
Done() <-chan struct{} // 取消通知 channel
Err() error // 取消原因
Value(key any) any // 携带的值
}
最常用的是 Done():返回一个 channel,context 被取消时这个 channel 会关闭。配合 select 监听它就行。
两个根 context
所有 context 都从一个根开始:
context.Background():用于 main 函数、初始化、测试,表示顶层context.TODO():还没想好用哪个时,先用它占位
两者本质都是空的 context,不能取消、没有 deadline、没有值。区别只在语义—告诉读代码的人「这是根」还是「这里待完善」。
ctx := context.Background() // 顶层根
ctx = context.WithCancel(ctx) // 派生可取消的
WithCancel 手动取消
WithCancel 返回一个 context 和一个取消函数:
func main() {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
time.Sleep(2 * time.Second)
cancel() // 取消,worker 收到信号退出
time.Sleep(time.Second)
}
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("收到取消,退出:", ctx.Err())
return
default:
fmt.Println("工作中...")
time.Sleep(500 * time.Millisecond)
}
}
}
worker 里用 select 监听 ctx.Done(),一旦关闭就退出。
Warning
cancel函数一定要调用,哪怕没手动取消也要defer cancel()。否则 context 持有的资源不会释放,导致泄漏。
WithTimeout 超时取消
WithTimeout 在指定时间后自动取消,不用手动调 cancel:
func main() {
// 1 秒后自动取消
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
go slowTask(ctx)
<-ctx.Done()
fmt.Println("主程序退出")
}
func slowTask(ctx context.Context) {
select {
case <-time.After(3 * time.Second):
fmt.Println("任务完成")
case <-ctx.Done():
fmt.Println("超时取消:", ctx.Err()) // context deadline exceeded
}
}
任务要 3 秒,1 秒就超时了,所以走 ctx.Done() 分支。
WithDeadline 指定时刻
WithDeadline 和 WithTimeout 类似,区别是它接收一个具体时间点:
// 今晚 23:59:59 取消
deadline := time.Date(2026, 7, 27, 23, 59, 59, 0, time.Local)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
WithTimeout 其实就是 WithDeadline(parent, time.Now().Add(timeout)) 的简写。
WithValue 携带值
WithValue 给 context 附带键值对,在 goroutine 间传递请求级别的数据:
func main() {
ctx := context.WithValue(context.Background(), "userID", 42)
handleRequest(ctx)
}
func handleRequest(ctx context.Context) {
uid := ctx.Value("userID")
fmt.Println("用户ID:", uid) // 42
}
Warning
WithValue不是用来传业务参数的!它只适合传请求级别的元数据,比如 trace ID、用户身份、认证 token。别用它代替函数参数传业务数据,那会让代码难以理解和测试。
context 的传播
context 的核心是「传播」:父 context 取消,所有子 context 都取消。
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 从 ctx 派生子 context
childCtx, childCancel := context.WithCancel(ctx)
defer childCancel()
go func() {
<-childCtx.Done()
fmt.Println("child 收到取消")
}()
cancel() // 取消父,child 也会收到
time.Sleep(time.Second)
}
Background
|
WithCancel (cancel调用了)
|
WithCancel (子,自动跟着取消)
|
worker goroutine 监听 Done
这就是为什么 HTTP handler 第一个参数总是 ctx context.Context—请求取消时,整条调用链都能收到。
取消原因
ctx.Err() 返回取消原因:
context.Canceled:被cancel()主动取消context.DeadlineExceeded:超时
Go 1.21 起还能用 context.Cause(ctx) 获取更具体的原因(配合 WithCancelCause 等)。
小结
Background()是顶层根,TODO()是占位WithCancel:手动取消,cancel()触发WithTimeout/WithDeadline:自动超时取消WithValue:携带请求级元数据,别传业务参数- context 传播:父取消,所有子跟着取消
cancel函数必须调用,用defer兜底
下一节讲 context 进阶用法和最佳实践。