close(ch)在高并发下会出问题,因为关闭通道后所有阻塞接收者被唤醒,但若未配合select default或done检查,易引发重复处理、panic(如关闭已关通道)或竞态;且无法区分“取消”与“正常结束”,缺乏上下文传递能力。

为什么用 close(ch) 做取消在高并发下会出问题
关闭一个 chan struct{} 确实能立刻唤醒所有阻塞在 `close(ch) 和 context 混用(比如先 close 自己的 done chan,再调 cancel()),监听逻辑就变成竞态判断,ctx.Err() 和 ok == false 可能错位,导致部分 goroutine 退出时漏掉数据库连接释放或文件句柄关闭。
select { case 是高并发取消唯一一致的写法
context 的 Done() 返回只读 channel,底层由 runtime 统一管理,所有监听者看到的是同一信号源,且关闭时机严格受控(cancel() 或超时触发)。高并发下一致性来自三点:
- 每个 goroutine 都必须把
ctx.Done()和业务 channel 放进同一个select,不能拆开或嵌套 - 不能在
default分支里跳过取消检查——否则循环中一次没进case ,就可能卡死几十秒 - HTTP、SQL、time.Sleep 等阻塞操作必须用带 context 的版本(如
http.NewRequestWithContext、db.QueryContext、time.AfterFunc),否则它们根本不会响应ctx.Done()
高并发场景下 ctx 泄漏比逻辑错误更隐蔽
并发数一上去,context.WithCancel 创建的 timer 和 goroutine 就成倍增长。常见泄漏点:
- 在 for 循环里反复调
context.WithCancel(parent)却忘了 defer cancel(),timer 积压不释放 - goroutine 启动后没传 ctx 进去,而是内部重新
context.Background(),导致上游 cancel 完全无效 - 子 goroutine 又 spawn 新 goroutine,但只传了原始 parentCtx,没把当前 taskCtx 往下链式传递
泄漏不会立刻 panic,但会拖慢 GC、耗尽文件描述符、让 pprof 显示大量 idle timer —— 最难 debug 的是:看起来任务“取消了”,其实后台还在跑。
别在 select 外判断 ctx.Err() != nil
这是高并发下最常被忽略的一致性陷阱。例如写成:
if ctx.Err() != nil {
return
}
// 然后才进 select
这会导致两个问题:
- 在
ctx.Err()变非 nil 到下一次循环之间,goroutine 仍可能执行一轮完整逻辑(比如发一次 HTTP 请求) - 如果
ctx是WithTimeout,而当前时间刚好卡在超时临界点,ctx.Err()可能短暂为 nil,然后下一毫秒才变context.DeadlineExceeded,中间窗口期就是竞态
正确姿势永远是:所有取消响应必须收束在 case 分支里,且该分支内做完清理再 <code>return。哪怕只是打印日志,也要在这条路径上做。


















