context.withcancel 不能自动传播取消信号,因 context 仅是信号载体,goroutine 必须主动检查 ctx.done() 并退出;常见错误是 select 中遗漏 case

Go context.WithCancel 为什么不能自动传播到所有 goroutine
因为 context.Context 本身不带执行控制能力,它只是个信号载体;goroutine 是否响应取消,完全取决于你有没有在关键位置检查 ctx.Done() 并主动退出。
常见错误现象:select 里漏了 case ,或者只在函数开头检查一次、后续循环中不再轮询;结果上游已取消,下游 goroutine 还在疯狂跑数据、占内存、发请求。
- 必须在每个可能阻塞或耗时的操作前,插入
select或if ctx.Err() != nil判断 - 尤其注意 for 循环内部——不能只在循环外 check 一次
- 如果用了
time.Sleep、http.Client.Do、chan recv/send,都要确保它们能被ctx中断(例如用time.AfterFunc替代裸 sleep,用http.NewRequestWithContext)
Pipeline 阶段间 chan 要用带缓冲的还是无缓冲的
取决于阶段处理速度是否稳定、是否允许背压传递。无缓冲 chan 强制同步,天然支持“上游等下游就绪”,但容易卡死;带缓冲 chan 可缓解瞬时抖动,但会掩盖阻塞问题,甚至导致内存泄漏。
使用场景:当某个阶段偶尔慢(比如网络 IO 波动),用小缓冲(如 1–4)可避免 pipeline 整体停摆;但若下游长期积压,缓冲区填满后写操作会阻塞,此时必须靠 ctx.Done() 才能唤醒。
立即学习“go语言免费学习笔记(深入)”;
- 默认优先选无缓冲
chan int—— 行为可预测,取消信号能立刻反映在阻塞点上 - 加缓冲仅用于明确知道“下游延迟有界且短暂”,例如日志聚合阶段暂存几条记录
- 绝不要用
make(chan T, math.MaxInt)或大缓冲(如 10000+),这等于放弃背压,取消可能永远不生效
下游 goroutine 退出时要不要 close 输出 chan
要,但只能由**唯一确定的发送方**关闭,否则 panic:send on closed channel。Pipeline 中,通常由当前阶段的主 goroutine(即启动 worker 的那个)负责 close 自己的输出 chan。
容易踩的坑:多个 goroutine 同时往一个 chan 发数据,又都试图 close 它;或者上游已 close 输入 chan,下游误以为该轮到自己 close 输出 chan,结果和别的 worker 冲突。
- 每个阶段定义自己的输出 chan,并由该阶段的启动函数(如
stage1(in )在 defer 中 close - worker goroutine 只管 send,不负责 close
- 接收方永远不要 close 收到的 chan —— 它不是你创建的
cancel 后如何确保所有 goroutine 真的退出了
没有银弹。Go 没有内置的 goroutine join 机制,得靠组合手段验证:等待 + 信号 + 有限超时。
性能影响:加等待逻辑本身会拖慢正常退出路径,所以只应在测试或关键清理阶段启用;生产代码中更依赖“正确检查 ctx”来保证终态,而非强等。
- 用
sync.WaitGroup记录启动的 worker 数,在 cancel 后调用wg.Wait(),配合defer wg.Done() - 每个 worker 在退出前向一个 done chan 发信号,主协程用
for i := 0; i 收集 - 务必设超时(如
time.After(5 * time.Second)),防止因漏检 ctx 导致永久 hang
最常被忽略的一点:子 goroutine 启动了新的 goroutine(比如日志上报、metric 上报),却没把 ctx 传下去,导致 cancel 后这些“孙辈”还在跑。Pipeline 的每一层,只要 spawn 新协程,就必须显式传递上下文。











