调用 cancel() 后 goroutine 仍运行,因 Go 不支持抢占式取消,需在 goroutine 内每个阻塞点显式监听 ctx.Done(),如 select 中与 channel 操作、time.After、http.Client.Do 等并列,且不可遗漏循环中的检查。

不能靠 cancel() 本身让 goroutine 立刻停住,必须在 goroutine 内部主动监听 ctx.Done() 并响应。
为什么调用 cancel() 后 goroutine 还在跑
最常见原因是:goroutine 没在阻塞点监听 ctx.Done(),或者只监听了一次、后续循环里漏了。Go 不支持抢占式取消,cancel() 只是往 ctx.Done() 发一个关闭信号,goroutine 得自己去“看”——而且得在每个可能卡住的地方都看。
- 系统调用(如
net.Conn.Read、http.Get)不自动响应 context,必须显式传入带 ctx 的版本(如http.Client.Do) - 纯
time.Sleep不响应 context,得换成select+time.After+ctx.Done() - channel 操作(
<-ch或ch <-)也不自动响应,必须放进select分支里和ctx.Done()并列 - 如果用了
default分支做轮询,会 CPU 空转,且根本等不到取消信号
context.WithCancel 的正确使用姿势
它不是“开个开关就完事”,而是构建一个可传播的取消树。关键在父子关系和调用时机。
- 用
context.Background()或context.TODO()作为根,再用context.WithCancel()派生子 context - 把子
ctx传给 goroutine,但别只传进去就不管——goroutine 内必须有select { case - 父 context 被
cancel()后,所有子 context 的Done()通道也会被关闭,天然级联生效 - 只调用一次
cancel();第二次会 panic,建议用sync.Once包一层或确保只在明确退出路径上调用
哪些地方最容易漏掉 ctx.Done() 监听
不是“写了 select 就安全”,而是每个可能长期阻塞的分支前,都得有 ctx.Done() 的 case。常见盲区:
立即学习“go语言免费学习笔记(深入)”;
-
io.ReadAll(resp.Body):它内部是循环读,不检查 context。必须拆成for { n, err := resp.Body.Read(buf); if err != nil { break }; select { case -
database/sql查询:用db.QueryContext(ctx, ...)替代db.Query(...) - 自定义 channel 通信:如果 goroutine 在等
<-inCh,就得写成select { case v := - defer 里关资源:如果 defer 依赖 context(比如
http.Server.Shutdown(ctx)),要先判断ctx.Err() == nil,否则可能 panic 或无效
真正难的不是调 cancel(),而是在 goroutine 的每一处“可能卡住”的逻辑间隙,都插进对 ctx.Done() 的检查——这要求你对执行流足够熟悉,也意味着取消不是加一行代码的事,而是重构整个等待逻辑。


















