context.WithCancel不能直接终止goroutine,仅通过关闭Done通道发送取消信号;子协程必须主动监听ctx.Done()并自行退出,否则无法响应取消、释放资源或中断阻塞操作。

context.WithCancel 是唯一能让你在运行时主动触发协程退出的原生机制,它不杀协程,只发信号;你必须在子协程里监听 ctx.Done() 并自行退出。
为什么不能直接用 cancel() 就完事?
调用 cancel() 只是关闭 ctx.Done() 通道,它本身不做任何清理、不中断阻塞调用(比如 http.Get、time.Sleep、io.Read),也不保证子协程立刻停止。
子协程是否响应、何时退出、是否释放资源,全取决于你有没有在合适位置检查 ctx.Done() 并做对应处理。
常见错误现象:
– 协程仍在后台打印日志或发请求,但主逻辑已认为“已取消”
– http.Client 未传入 context,导致超时/取消信号被忽略
– 在非 select 块中轮询 ctx.Err() == context.Canceled,效率低且易漏判
context.WithCancel 的正确使用姿势
核心原则:每个派生出的 context.Context 都应有明确的生命周期归属,并由创建者负责调用 cancel()。
– 必须用 defer cancel() 包裹在创建它的函数末尾(除非你明确需要延迟取消)
– 不要把 cancel 函数暴露给下游协程,否则可能被误调或重复调用
– 若需跨 goroutine 触发取消,把 cancel 存为局部变量或闭包捕获,而非全局或结构体字段
– 启动子协程时,务必把 ctx 作为第一个参数传入,不要依赖闭包捕获外部 ctx
阻塞操作怎么配合 ctx.Done()?
Go 标准库多数 I/O 接口支持 context,但不是所有地方都自动感知:
– http.NewRequestWithContext(ctx, ...) → http.DefaultClient.Do(req) 才会响应取消
– time.Sleep 不接受 context,得改用 time.AfterFunc 或 select + time.After
– net.Conn 没有 context 参数,但 conn.SetDeadline 可配合 ctx.Deadline() 手动设置
– 自定义 long-running loop 必须用 select,且 case 要放在 default 前(避免忙等)<br>
示例片段:
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
log.Println("worker exiting:", ctx.Err())
return
default:
// do work
time.Sleep(100 * time.Millisecond)
}
}
}
容易被忽略的泄漏点
context 泄漏往往不是因为没调 cancel(),而是调得太晚或根本没调:
– 在 goroutine 内部调用 context.WithCancel,却没在 goroutine 退出前调 cancel()
– 把 ctx 传给第三方库后,丢失了对 cancel 的控制权
– 多层派生 context(如 WithValue → WithCancel → WithTimeout),只 cancel 最外层,内层仍存活
– HTTP handler 中用 req.Context() 派生新 context,但没在 handler 返回前调 cancel()(虽然通常由 net/http 自动处理,但自定义中间件时易出错)
真正难的不是写 cancel(),是判断“现在该不该取消”以及“取消后资源是否已释放”。ctx.Err() 返回值只有两个有意义状态:context.Canceled 和 context.DeadlineExceeded,其余都是 nil 或空,别靠它做业务分支。
文章来自机圈观察员网,发布者:,转载请注明出处:https://www.jqgcy.com/xinjizixun/116387.html