子函数收不到取消信号的根本原因是使用了context.background()或context.todo()等不可取消的emptyctx,而非通过context.withcancel/withtimeout/withdeadline派生的可取消context;必须传递可关闭done()通道的子context,并在子函数中用select监听

为什么子函数收不到取消信号?
根本原因往往是子函数没拿到带取消能力的 context.Context,而是用了 context.Background() 或 context.TODO() ——它们返回的是 emptyCtx,Done() 永远不会关闭,自然收不到信号。
- 必须从
context.WithCancel、context.WithTimeout或context.WithDeadline开始派生,才能获得可关闭的Done()channel - 子函数只接收
ctx参数,不持有cancel函数;取消权必须保留在调用方 - 如果子函数内部又启动了 goroutine,得把当前
ctx传进去,或用context.WithValue等派生新 context,不能漏传
子函数里怎么监听取消并安全退出?
监听不是“查一次 ctx.Err() 就完事”,而是要阻塞等待 ctx.Done() 关闭信号,否则会漏掉、或忙等耗 CPU。
- 必须用
select等待,这是唯一可靠入口 - 循环中别写
if ctx.Err() != nil { break }——它只能检测已发生的错误,无法响应未来取消 - 若内部有阻塞调用(如
http.Client.Do、time.Sleep、ch ),确保传入 <code>ctx,让底层支持取消(例如用req.WithContext(ctx)) - 收到
Done()后,别再往已关闭的 channel 发送数据,否则 panic
多层嵌套时如何保证全链路响应?
靠 context 的树形继承机制,不是靠手动广播。只要每层都正确派生和传递,cancel() 一调,所有监听者同时感知。
- 每个新 goroutine 启动时,都应基于上一级
ctx派生(哪怕只是context.WithValue(ctx, k, v)),而不是复用原始ctx或硬编码context.Background() -
WithCancel/WithTimeout返回的新ctx共享同一个Done(),所以子协程监听各自拿到的ctx.Done()即可 - 父协程自己也要响应取消(比如等待子 goroutine 退出),记得用
defer cancel()防泄漏,且不要在cancel()后继续使用该ctx
cancel() 调用后还有哪些容易忽略的细节?
取消信号发出去只是第一步,后续资源清理和状态检查才是关键,这里最容易出问题。
-
cancel()只关闭Done()channel,不自动释放文件句柄、数据库连接等——必须在defer中显式关闭 - 判断取消原因要用
ctx.Err(),而不是err == context.Canceled:超时是context.DeadlineExceeded,手动取消才是context.Canceled -
cancel()是幂等的,但重复调用说明逻辑有问题;建议用sync.Once封装,避免误触发 - 别把
cancel函数存进结构体或全局变量——它只该被调用一次,且应由创建它的作用域控制











