应使用 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 放进同一个 <code>select,确保原子性响应 - 收到
ctx.Done()后,要检查ctx.Err()是context.Canceled还是context.DeadlineExceeded,决定是否记录日志或清理资源 - 如果业务 channel 是带缓冲的,注意
select仍可能随机选择就绪分支,不能假设执行顺序
select {
case data := <-workCh:
process(data)
case <-ctx.Done():
log.Println("canceled:", ctx.Err())
return
}
不要在 ctx.Done() 触发后继续往业务 channel 发送数据
一旦 ctx.Done() 被关闭,监听它的 goroutine 应尽快退出。此时若上游还在往 workCh 写数据,而下游已退出,channel 就会阻塞或 panic(如果带缓冲且满)。
常见陷阱:用 for range ch 读 channel,却没配合 context 控制循环生命周期——range 不感知 context,会一直等到 channel close,但 cancel 时你可能根本不想等它关。
- 发送方应在每次发送前检查
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()当占位符传给生产逻辑——它只是开发阶段的标记,运行时无实际作用


















