ctx.Done()返回的channel只在取消、超时或截止到达时关闭;Background和TODO的Done()返回nil,select读nil channel会永久阻塞。
ctx.done() 返回的 channel 不是“一创建就可读”,它只在 context 被取消、超时或截止时间到达时关闭;background 和 todo 的 done() 永远不关闭(返回 nil),拿它们做 select 会永久阻塞。
为什么 select
这不是 channel 本身有问题,而是触发条件没满足:
- 忘了调用
cancel()—— 比如用context.WithCancel创建子 context 后,没在合适时机显式调用返回的cancel函数 - 用了
context.WithTimeout(ctx, 5*time.Second),但还没到 5 秒,Done()自然不会关 - 误把
context.Background().Done()或context.TODO().Done()当成可关闭 channel 用,它们返回nil,select会直接跳过该 case(如果没 default)或永远挂起(如果有 default 但逻辑写错) - 在 goroutine 启动前就提前读了一次
ctx.Done()并缓存为变量,而此时 channel 还没关,后续select仍等同一个未关闭的 channel
Done() 必须和 Err() 配合使用
仅靠 只能知道“该停了”,但不知道“为什么停”:
-
ctx.Err()是同步函数,返回context.Canceled或context.DeadlineExceeded,必须用errors.Is(err, context.Canceled)判断,不能用== - 常见错误:在 for 循环里轮询
ctx.Err() != nil,导致空转 CPU;正确做法是select等,退出后再查 <code>ctx.Err() - 如果
ctx.Done()已关闭,ctx.Err()一定非 nil;但如果ctx.Err()是 nil,不能反推ctx.Done()一定没关(极端情况下存在极短窗口,但实践中可忽略)
自定义 Context 时 Done() 的生命周期怎么管?
实现 Context 接口时,Done() 返回的 channel 必须满足两个硬性要求:
- 一旦关闭,不可重开;多次调用
Done()必须返回同一个底层 channel(否则监听失效) - 不能在构造函数里就 close,也不能漏 close——比如你封装了一个带超时逻辑的自定义
timerCtx,但忘了在计时器触发时调用close(ch),下游 goroutine 就永远收不到通知 - 如果内部用
chan struct{},记得用make(chan struct{}, 1)或无缓冲 channel,别用带缓冲且容量 >1 的,否则可能多发信号或漏通知
最易被忽略的一点:Done channel 的关闭时机必须与 Err() 的返回值严格对齐。runtime 的标准实现里,cancelCtx.cancel 函数里先 close channel,再原子更新 err 字段;如果你手动实现,顺序颠倒或不同步,会导致 select 收到关闭信号后,ctx.Err() 还是 nil,无法判断原因。


















