应使用 context.Context 的 Done() channel 实现取消通知,而非自建 cancelCh;所有 goroutine 需在 select 中并列监听
用 context.Context 配合 channel 实现取消通知,不是用 channel 自己做取消
go 里 channel 本身不带“取消”语义,硬靠 close(ch) 或往里塞特殊值来通知取消,容易引发 panic 或竞态。真正优雅的做法是把 context.context 的 done() channel 作为信号源,让 goroutine 监听它,而不是自己造一套取消 channel。
常见错误是:启动 goroutine 后单独开一个 cancelCh chan struct{},然后到处传、手动 close——这既难追踪生命周期,又容易漏关或重复关。
-
context.WithCancel返回的ctx.Done()是只读、单向、线程安全的,天然适配 select 场景 - 所有需要响应取消的 goroutine,统一在
select中监听,无需额外 channel 管理 - 父 goroutine 调用
cancel()后,所有监听该ctx的地方会同时收到信号,语义清晰
select 中必须同时监听 ctx.Done() 和业务 channel
只监听 ctx.Done() 会导致任务没机会执行;只监听业务 channel 又无法响应取消。正确姿势是在同一个 select 块里并列处理。
典型错误:先用 if ctx.Err() != nil 检查再读 channel——这中间存在竞态窗口,可能刚检查完 context 就被 cancel,但 goroutine 还卡在 上。
- 永远把
和业务 channel 放进同一个select,确保原子性响应 - 收到
ctx.Done()后,要检查ctx.Err()是context.Canceled还是context.DeadlineExceeded,决定是否记录日志或清理资源 - 如果业务 channel 是带缓冲的,注意
select仍可能随机选择就绪分支,不能假设执行顺序
select {
case data := <h3>不要在 <code>ctx.Done()</code> 触发后继续往业务 channel 发送数据</h3><p>一旦 <code>ctx.Done()</code> 被关闭,监听它的 goroutine 应尽快退出。此时若上游还在往 <code>workCh</code> 写数据,而下游已退出,channel 就会阻塞或 panic(如果带缓冲且满)。</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/learn/7564" title="使用Go语言搭建家庭相册系统-相关课件"><img
src="https://img.php.cn/upload/webcode/000/000/164/636a2b4d84031727.png" alt="使用Go语言搭建家庭相册系统-相关课件" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/learn/7564" title="使用Go语言搭建家庭相册系统-相关课件" class="overflowclass">使用Go语言搭建家庭相册系统-相关课件</a>
<p class="overflowclass">使用Go语言搭建家庭相册系统-相关课件</p>
</div>
<a rel="nofollow" href="/xiazai/learn/7564" title="使用Go语言搭建家庭相册系统-相关课件" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div><p>常见陷阱:用 <code>for range ch</code> 读 channel,却没配合 context 控制循环生命周期——range 不感知 context,会一直等到 channel close,但 cancel 时你可能根本不想等它关。</p>
- 发送方应在每次发送前检查
ctx.Err() == nil,不满足则跳过发送 - 更稳妥的是用
select配合default分支实现非阻塞发送,避免 goroutine 卡住 - 如果必须保证数据送达,应改用带超时的
select,而非依赖 context 取消后继续发
跨 goroutine 传递 context 时,别用 background 或 todo 替代真实上下文
有人图省事,在函数参数里写 func doWork(ctx context.Context),调用时传 context.Background(),结果整个链路失去取消能力。真正需要取消的地方,必须由最外层创建可取消的 context,并逐层透传。
另一个典型问题:在 http handler 里拿到 r.Context(),却用它启动一个长期后台 goroutine,却不复制子 context(如用 context.WithTimeout),导致 handler 结束后 goroutine 仍持有已 cancel 的 ctx,反复触发 ctx.Done()。
- HTTP handler 中启动异步任务,务必用
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)创建子 context - 子 goroutine 退出时记得调用
cancel()(如果没被父 context cancel 掉),避免 goroutine 泄漏 - 不要把
context.TODO()当占位符传给生产逻辑——它只是开发阶段的标记,运行时无实际作用
context 的取消信号本质是“通知”,不是“命令”。真正难的不是怎么发信号,而是每个接收方是否在恰当位置监听、是否及时释放资源、是否避免对已取消 context 做无效操作。这些细节不写进代码注释,很容易被下个维护者忽略。










