ctx.Done()仅在context被取消、超时或截止时间到达时由runtime自动关闭;Background和TODO的Done()永不关闭,提前缓存nil或过早读取会导致select永久阻塞。
ctx.done() 关闭不是你手动关的,而是 context 被取消或超时后 runtime 自动关闭的;它只在该事件发生时才变为可读,之前 select 会一直阻塞——这不是 bug,是设计使然。
ctx.Done() 什么时候真正关闭
它只在以下三种情况之一发生时关闭:
- 调用
cancel()函数(来自context.WithCancel) -
context.WithTimeout或context.WithDeadline设置的时间到达 - 父 context 关闭,且当前 context 是其子 context(多级传播)
注意:context.Background() 和 context.TODO() 的 Done() 永远不关闭(返回 nil channel),拿它们做 select 会永久阻塞。常见错误是没调用 cancel() 就等信号,结果什么也没发生。
为什么不能提前缓存 ctx.Done() 后再用
缓存本身不违法,但容易踩两个坑:
- 从
Background或未派生的 context 缓存,得到nil,后续select { case 直接 panic - 在 goroutine 启动前就执行一次
(比如调试时打印),导致 channel 已被“消费”过一次,后续 select 失效
安全做法是:在关键路径上原地写 ,或确保缓存发生在子 context 创建后、goroutine 启动前的瞬间(例如 <code>done := ctx.Done() 紧跟 ctx, cancel := context.WithCancel(parent) 之后)。
select 中必须同时监听 ctx.Done() 和业务 channel
只监听 :任务根本不会执行;只监听业务 channel:无法响应取消。正确结构是:
select {
case data := <-workCh:
process(data)
case <-ctx.Done():
log.Println("canceled:", ctx.Err())
return
}
常见错误写法:
- 先
if ctx.Err() != nil再读workCh—— 中间存在竞态窗口,可能刚检查完就被 cancel,但 goroutine 还卡在 - 用
for range workCh却不配合ctx.Done()——range不感知 context,cancel 后仍等 channel close,而你根本不想等
若 workCh 是带缓冲的,注意 select 仍可能随机选分支,不能靠顺序假设执行逻辑。
收到 ctx.Done() 后该做什么
收到信号只是开始,不是终点:
- 必须立刻检查
ctx.Err(),用errors.Is(err, context.Canceled)或errors.Is(err, context.DeadlineExceeded)判断原因,决定是否记录日志或触发清理 - 不能再往任何业务 channel 发数据 —— 下游 goroutine 可能已退出,导致发送阻塞或 panic(尤其无缓冲 channel)
- 若持有文件、连接、timer,需显式
defer或同步关闭,goroutine 退出不等于资源自动释放
最易忽略的一点:ctx.Done() 是通知“该停了”,ctx.Err() 才告诉你“为什么停”。两者必须配对使用,缺一不可。


















