context
需要预先了解:
1. context
一个请求往往要派生多个 goroutine:查数据库、调下游服务、写日志。如果其中一步超时或客户端断开,其他步骤需要尽快停下。context 就是用来统一传递取消信号、截止时间和请求范围内的元数据的标准机制。
1.1 需求引入
假设一个函数要并发做三件事,任何一件失败或超时就整体放弃。如果每个 goroutine 各自用 done channel,代码会变成一堆手写的通知逻辑:
done1 := make(chan struct{})
done2 := make(chan struct{})
// 谁来关闭?超时怎么办?取消要不要级联?
context 把这些问题标准化:它是一棵可级联取消的树,父节点取消时所有子节点都会收到通知。
1.2 Context 接口
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
四个方法的职责:
| 方法 | 作用 |
|---|---|
Deadline |
返回截止时间,第二个返回值表示是否设置了截止时间 |
Done |
返回一个 channel,context 被取消时它被关闭 |
Err |
context 结束后返回原因:context.Canceled 或 context.DeadlineExceeded |
Value |
取出与该 context 关联的值 |
Done 是唯一必须用的方法——它让你能 select 等待取消。Err 通常在收到取消后调用,用来区分"被主动取消"和"超时"。
1.3 根 context
所有 context 树都从两个根之一开始:
context.Background():主函数、初始化、测试中作为根使用;context.TODO():暂时不知道该传什么时的占位。
两者都是空的、不可取消的 context,行为完全一样,区别只在语义:TODO 表示"这里以后要换成真正的 context"。
1.4 派生 context
WithCancel
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 一定要调用,否则会泄漏
cancel() 关闭 ctx.Done(),并把取消信号传播给所有派生子节点。
WithTimeout / WithDeadline
ctx, cancel := context.WithTimeout(parent, 50*time.Millisecond)
defer cancel()
ctx2, cancel2 := context.WithDeadline(parent, time.Now().Add(30*time.Millisecond))
defer cancel2()
WithTimeout 就是 WithDeadline(parent, time.Now().Add(d)) 的简写。超时后 ctx.Err() 返回 context.DeadlineExceeded。
即使设置了超时,也要 defer cancel():提前完成时取消可以释放定时器资源,避免等到超时才被 GC。
WithValue
type key string
ctx := context.WithValue(context.Background(), key("traceID"), "abc-123")
if v, ok := ctx.Value(key("traceID")).(string); ok {
fmt.Println("traceID =", v) // abc-123
}
key 建议用自定义类型(比如 type key string)而不是裸字符串,避免不同包之间撞 key。Value 里只放请求范围的元数据(trace ID、用户身份、认证信息),不要放业务参数或可选参数。
树形结构与传播
context.Background()
|
WithCancel(请求级)
/ \
WithTimeout WithValue
| |
worker A worker B
传播规则:
- 取消父节点 → 所有子孙节点都被取消;
- 取消子节点 → 父节点和其他兄弟节点不受影响。
parent, cancelParent := context.WithCancel(context.Background())
child1, cancel1 := context.WithCancel(parent)
child2, cancel2 := context.WithCancel(parent)
defer cancel1()
defer cancel2()
cancelParent()
time.Sleep(50 * time.Millisecond)
fmt.Println("child1.Err() =", child1.Err()) // context canceled
fmt.Println("child2.Err() =", child2.Err()) // context canceled
反向取消:
child3, cancel3 := context.WithCancel(parent2)
cancel3()
fmt.Println("child3.Err() =", child3.Err(), "parent2.Err() =", parent2.Err())
child3.Err() = context canceled parent2.Err() = <nil>
1.5 使用惯例
ctx作为函数的第一个参数,命名为ctx:func Fetch(ctx context.Context, id int) (*User, error)- 不要把
ctx存进结构体。它是请求级对象,存进结构体会让生命周期难以追踪。例外是http.Request这类本身携带请求上下文的结构体。 ctx可以为 nil 吗? 不可以,绝大多数函数会直接调用ctx.Done(),传 nil 会 panic。不确定时传context.TODO()。- 只在真正需要取消的层级派生。不要每个函数都
WithCancel,否则取消链很长,cancel容易忘记调用。 Value不要用来传业务参数。业务参数应该显式出现在函数签名里,否则调用方无法从签名知道依赖了什么。
1.6 新增 API
WithCancelCause 与 Cause(Go 1.20+)
默认的取消原因是 context.Canceled,无法区分"为什么取消"。带 cause 的版本可以携带具体错误:
ctx, cancel := context.WithCancelCause(context.Background())
cancel(errors.New("数据库连接失败"))
<-ctx.Done()
fmt.Println("ctx.Err() =", ctx.Err()) // context canceled
fmt.Println("context.Cause =", context.Cause(ctx)) // 数据库连接失败
ctx.Err() 仍然是 context.Canceled,具体原因通过 context.Cause(ctx) 获取。
AfterFunc(Go 1.21+)
在 context 被取消时执行一个函数,不需要自己起 goroutine 等待 Done:
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
context.AfterFunc(ctx, func() { close(done) })
cancel()
<-done
fmt.Println("AfterFunc 的回调被触发")
返回值 stop 可以取消这个注册:
stop := context.AfterFunc(ctx2, func() { fmt.Println("这行不会打印") })
fmt.Println("stop() 返回值:", stop()) // true
WithoutCancel(Go 1.21+)
派生一个不随父节点取消的 context,但保留其中的值:
parent, cancelParent := context.WithCancel(context.Background())
ctx := context.WithValue(parent, "k", "v")
detached := context.WithoutCancel(ctx)
cancelParent()
fmt.Println("parent.Err() =", parent.Err()) // context canceled
fmt.Println("detached.Err() =", detached.Err()) // <nil>
fmt.Println("值仍然保留:", detached.Value("k")) // v
典型用途:请求结束后仍要写审计日志、上报指标——这些操作不应该因为客户端断开就被取消。
1.7 可取消的 worker
把 context 和 select 组合起来,就是最常见的可取消任务模式:
func worker(ctx context.Context, id int) {
for i := 1; ; i++ {
select {
case <-ctx.Done():
fmt.Printf("worker %d 收到取消信号: %v\n", id, ctx.Err())
return
default:
time.Sleep(30 * time.Millisecond)
fmt.Printf("worker %d 完成第 %d 轮\n", id, i)
}
}
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go worker(ctx, 1)
go worker(ctx, 2)
time.Sleep(80 * time.Millisecond)
cancel()
time.Sleep(50 * time.Millisecond)
输出(顺序不固定):
worker 1 完成第 1 轮
worker 2 完成第 1 轮
worker 2 完成第 2 轮
worker 1 完成第 2 轮
取消 parent
worker 2 完成第 3 轮
worker 2 收到取消信号: context canceled
worker 1 完成第 3 轮
worker 1 收到取消信号: context canceled
关键点:
select同时等待"取消信号"和"继续工作";- 收到取消后立即
return,不要再启动新的工作; - 如果 worker 阻塞在 IO 上,要把
ctx传给 IO 函数(标准库的http.NewRequestWithContext、db.QueryContext都接收ctx)。
1.8 标准库中的 ctx
context 是标准库的一等公民,很多 API 都带 ctx 参数:
// net/http:带超时的请求
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := http.DefaultClient.Do(req)
// database/sql:带取消的查询
row := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = ?", id)
// 服务端从请求里取 context
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// ...
}
HTTP 服务的优雅关闭、请求超时、下游调用取消,都建立在这套机制上,详见 标准库(二):JSON 与 HTTP 服务。
注:
context只负责"通知取消",不负责"强制终止"。收到取消信号后是否立刻停止,由你的代码决定。定时器、channel、IO 都要显式处理取消,否则 goroutine 依然会泄漏。
1.9 练习
- 实现一个函数,用
context.WithTimeout限制总耗时,内部并发请求三个"慢服务"(用time.Sleep模拟),任何一个超时就返回错误。 - 用
WithValue传递一个 trace ID,在多个函数中通过ctx.Value取出并打印,验证它跨函数、跨 goroutine 都能访问到。 - 用
WithCancelCause实现一个"失败即取消全部"的批量任务,用context.Cause打印出第一个失败原因。



