ctx.Done() 关闭即时,但感知依赖监听时机;Background/TODO 的 Done() 返回 nil channel,select 中被忽略;取消需用 WithCancel/WithTimeout 派生上下文并正确监听。

ctx.Done() 关闭是即时的,但“感知到关闭”取决于你何时监听
ctx.Done() 通道本身在取消/超时触发瞬间就被 runtime 关闭,这个动作毫秒级、无延迟。问题往往不出在“它关得慢”,而在于你的 goroutine 没在正确时机或方式下读它。
常见错误现象:
-
select里写了case <-ctx.Done(),但程序一直卡在别的分支,没走到这行 - 启动 goroutine 前就缓存了
done := ctx.Done(),结果该 channel 是nil(比如来自context.Background()),导致select永远阻塞 - 在 for 循环里只轮询
ctx.Err() != nil,没用select等待信号,CPU 空转且无法及时退出
使用场景决定响应表现:
- HTTP handler 中用
r.Context().Done():请求被客户端断开时,通常 1–2 个 TCP RST 或 FIN 后即触发,实测多数在 100ms 内 -
context.WithTimeout(ctx, 500*time.Millisecond):计时器由 Go runtime 的 timer heap 管理,误差通常 < 1ms,超时后Done()立刻关闭 -
context.WithCancel()+ 手动调用cancel():调用返回即关闭,无任何可观测延迟
关键点:不是 Done() 慢,是你没让它参与调度路径。
Background 和 TODO 的 Done() 永远不关闭,别拿它们做取消判断
context.Background() 和 context.TODO() 的 Done() 方法返回 nil channel。这意味着:
- 在
select语句中写case <-ctx.Done()会直接忽略该分支(Go 规范定义:nilchannel 在select中永远不可读/不可写) - 如果你缓存了
done := ctx.Done()并期望它后续可读,那它永远不可读 - 这不是 bug,是设计:这两个 context 就是“永生”的根,不承载取消语义
容易踩的坑:
- 测试时随手传
context.Background()进函数,结果发现超时逻辑完全不生效 - 把
context.TODO()当占位符传给生产代码,上线后某层 goroutine 因没收到取消信号而泄漏 - 在中间件或封装函数里忘记校验
ctx.Done() != nil就直接select,导致静默失效
只要涉及取消、超时、截止时间,就必须用 WithCancel、WithTimeout 或 WithDeadline 派生的子 context。
多层派生 context 的 Done() 关闭几乎是同时的,但依赖每层主动监听
父 context 被 cancel 后,所有通过 context.WithCancel、context.WithTimeout 等创建的子 context,其 Done() 通道会在同一轮 goroutine 调度中被关闭——没有网络、没有 IPC、不跨系统调用,纯内存状态变更。
但“几乎同时”不等于“自动传播效果”。真实行为取决于:
- 每一层 goroutine 是否在关键路径上用了
select { case <-ctx.Done(): ... } - 是否在循环体内部每次迭代都检查,而不是只在入口 check 一次
- 是否把
ctx正确传进闭包、回调、子 goroutine,而不是捕获外层变量导致用错 context
典型泄漏场景:
- 主 goroutine 调用
cancel(),A 层 goroutine 收到并退出,但 A 启动的 B goroutine 拿的是原始context.Background(),继续跑 - HTTP handler 里用
ctx启了个 goroutine 做异步日志,但没把ctx传进去,超时后日志 goroutine 不停
取消信号不会自动钻进你没监听的地方;它只是一条静默的通道,等你伸手去摸。
自定义 Context 实现 Done() 时最容易漏掉 channel 生命周期管理
如果你实现了 Context 接口(比如封装 trace ID 或加审计逻辑),必须自己管理 Done() 返回的 channel:
- 不能每次调用都新建一个
chan struct{}:会导致监听者永远收不到关闭通知(旧 channel 没关,新 channel 没人监听) - 不能缓存未关闭的 channel 后忘记 close:goroutine 泄漏
- 必须确保
Done()返回同一个底层 channel 实例,且仅在确定要取消时调用close()
错误写法示例:
func (c *myCtx) Done() <-chan struct{} {
ch := make(chan struct{})
if c.shouldCancel {
close(ch)
}
return ch // 每次都新 channel,监听者永远等不到
}正确姿势要点:
- 在结构体字段中声明
done chan struct{} - 构造时初始化为
make(chan struct{}) - 取消逻辑里只调用一次
close(c.done) -
Done()方法直接返回c.done
标准库的 cancelCtx 和 timerCtx 都严格遵循这套规则;自己实现时最容易在“谁负责 close”和“channel 复用”上出错。


















