context.WithCancel 是唯一推荐的入口,应直接监听 ctx.Done() 并在 select 中与业务 channel 并列处理,所有读写操作需配合上下文判断 err 或使用 select+default 非阻塞,派生 context 必须透传不可替换为 Background() 或 TODO(),取消需全链路协同。

context.WithCancel 是唯一推荐的入口,别用自建 cancelCh 或全局变量控制 goroutine 退出。
直接监听 ctx.Done() 并在 select 中与业务 channel 并列处理,才是 Go 并发退出的正解。硬靠 close(cancelCh) 或轮询 exit bool,不是漏信号就是引发 panic。
select 必须同时监听 ctx.Done() 和业务 channel
只监听 ctx.Done(),goroutine 永远收不到数据;只监听业务 channel,取消信号根本进不来。
- 错误写法:
if ctx.Err() != nil { return }再读<-ch—— 中间存在竞态窗口,刚判完就 cancel,goroutine 卡死 - 正确写法:所有读操作必须包裹在同一个
select块里 - 示例:
select {
case data := <-workCh:
process(data)
case <-ctx.Done():
log.Printf("exiting: %v", ctx.Err())
return
}
- 如果
workCh是带缓冲的,select可能随机选分支,不能靠顺序做逻辑假设
发送方也要检查 ctx.Err(),不能只管发
下游 goroutine 一退出,上游若继续往 workCh 发数据,轻则阻塞,重则 panic(尤其带缓冲且满时)。
- 每次发送前加判断:
if ctx.Err() != nil { return } - 更稳妥做法:用
select+default实现非阻塞发送 - 错误模式:
for range ch读取却不配合 context ——range不感知 cancel,会一直等到 channel close,但你根本不想等
context.WithCancel 派生后必须透传,别中途换成 context.Background()
一旦用了 Background() 或 TODO() 替代真实上下文,取消信号就断了,子 goroutine 永远收不到通知。
- HTTP handler 中派生子 context,应使用
req.Context()而非context.Background() - 后台任务启动新 goroutine 时,必须用
context.WithXXX(parentCtx)派生,不能另起炉灶 -
cancel()是幂等的,可安全多次调用,但别忘了它只影响当前及子 context,不影响父级


















