context.WithCancel是级联取消的起点,其本质是父Context的Done通道关闭时,所有直接或间接子Context的Done通道自动关闭,形成单向树形传播;子Context取消不影响父或兄弟Context。

context.WithCancel 是级联取消的起点
级联取消的本质,是让子 context 跟随父 context 的 Done 通道关闭而自动关闭。这依赖 context.WithCancel 创建的父子关系:父 context 调用 cancel() 后,所有通过它派生的子 context 的 Done() 都会立即关闭。
- 每次调用
context.WithCancel(parent)返回一个新 context 和一个独立的cancel函数,这个 cancel 只影响当前分支,不影响父或其他兄弟分支 - 父 context 的
Done()关闭 → 所有直接/间接子 context 的Done()也关闭(无需手动调用子 cancel) - 但反过来不成立:子 cancel 不会影响父或兄弟,这是单向传播
parent, parentCancel := context.WithCancel(context.Background()) child1, _ := context.WithCancel(parent) child2, _ := context.WithCancel(parent) <p>parentCancel() // 此时 child1.Done() 和 child2.Done() 都已关闭
HTTP handler 中必须把 request.Context() 作为根 context 使用
Go 的 http.Request 自带 context(由 server 在请求开始时创建),它会在客户端断开、超时或主动取消时自动关闭。这是级联取消天然的入口点,不能丢弃或替换为 context.Background()。
- 错误做法:
ctx := context.Background()再传给下游,完全脱离 HTTP 生命周期 - 正确做法:始终以
r.Context()为起点派生,例如ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) - 如果 handler 启动 goroutine 处理耗时任务(如写日志、发通知),必须把
r.Context()传进去,并监听其Done(),否则可能在请求结束很久后还在执行
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // ✅ 唯一根 context
go func() {
select {
case <-time.After(10 * time.Second):
log.Println("task done")
case <-ctx.Done(): // ✅ 请求取消时立刻退出
log.Println("canceled:", ctx.Err())
}
}()
}
数据库查询和第三方 SDK 要显式传入 context 并检查 Done
很多 Go 生态库(如 database/sql、redis/go-redis、aws-sdk-go)都支持 context 参数。它们不是“自动响应取消”,而是靠你在调用时传入,并在内部轮询 ctx.Done() 或用 select 切换。
-
db.QueryRowContext(ctx, ...)比db.QueryRow(...)多一层 cancel 感知能力,但前提是底层驱动真正实现了 context 支持(多数主流驱动已支持) - 第三方 client 如
http.Client.Do(req.WithContext(ctx))必须显式把 context 绑到 request 上,否则 HTTP 请求不会因 context 取消而中断 - 若 SDK 不支持 context(老版本或小众库),需自行包装:启动 goroutine 执行操作,同时
select监听ctx.Done()并手动中止(如关闭连接、close channel)
常见错误现象:context deadline exceeded 报错没出现,但 goroutine 卡住不返回 —— 很可能是调用了无 context 版本的 API。
自定义操作中别忘了 select + Done 模式
不是所有操作都内置 context 支持。当你写一个需要长时间等待的函数(比如轮询状态、读取流、等待信号),必须手动集成取消逻辑。
- 核心模式永远是:
select在业务逻辑 channel 和ctx.Done()之间做选择 - 不要只在函数开头检查
ctx.Err(),那只能防“刚进来就取消”;必须在整个生命周期内持续监听 - 若操作涉及多个步骤(如先查 DB,再调 API,再发消息),每一步前都应检查
ctx.Err(),避免无效执行
func waitForEvent(ctx context.Context, ch <-chan Event) (*Event, error) {
for {
select {
case e := <-ch:
return &e, nil
case <-ctx.Done():
return nil, ctx.Err() // ✅ 实时响应取消
}
}
}
级联取消真正容易被忽略的点,是「中间层忘记透传 context」——比如封装了一个工具函数,参数里没加 ctx context.Context,或者调用下游时用了硬编码的 context.Background()。一旦断链,整个下游就脱离了请求生命周期控制。


















