高优 channel 必须前置,因 select 在多 case 就绪时伪随机选择,不按代码顺序执行;正确做法是将高优 case 置顶且唯一,否则低优 case 可能长期抢占导致高优任务饥饿。

select 中高优 channel 必须前置,否则随机性会破坏优先级
很多人写 select 时把低优 channel 放前面,以为“反正都写了”,结果高优任务长期被跳过。Go 的 select 在多个 case 就绪时是伪随机选取的,不是按顺序执行——它只保证“至少一个就绪就选一个”,不保证选哪个。
真正起作用的是代码书写顺序:高优 case 必须放在最上面,且只出现一次。重复写同一个 case 不会提高命中率,反而让逻辑难维护。
- 错误写法:
case t := 在前,<code>case t := 在后 → 高优大概率被忽略 - 正确写法:高优
case第一位置,低优在后,中间不插default - 如果高优 channel 缓冲已满,发送方阻塞,该
case就不算就绪,select自动跳过 → 所以缓冲大小得匹配吞吐,别设成 1 还指望“立刻响应”
用多个带缓冲 channel 分离优先级,而不是靠 runtime 控制 goroutine
Go 运行时没有 runtime.SetGoroutinePriority 这种 API,也永远不会有。所有试图给 goroutine “设优先级”的尝试(比如调用 runtime.Gosched() 或 time.Sleep(0))都是不可靠的,既不能插队,也不能抢 CPU 时间。
可行路径只有一条:把“高优 goroutine”转成“高优任务”,再用不同 channel 拆开。比如定义 highPrioCh 和 lowPrioCh,容量分别设为 10 和 50 —— 这不是随意拍的,而是基于预期并发峰值的 1.5–2 倍估算值。
立即学习“go语言免费学习笔记(深入)”;
- 太小:任务发不出去,调用方阻塞或丢数据
- 太大:延迟升高,内存占用不可控
- 别混用同一个 channel 处理不同优先级任务,否则优先级逻辑全失效
default 不是优先级开关,滥用会导致高优任务饿死
加 default 的本意是避免 select 阻塞,但它一旦执行,就意味着本轮什么都没处理。高频轮询 + default 看似“非阻塞”,实则容易让高优 channel 刚空闲一瞬就被跳过,延迟累积。
更稳妥的做法是用 time.After 或 time.Tick 控制等待窗口。例如每 100μs 检查一次高优 channel,空闲后再花 50μs 查中优,最后才看低优 —— 权重体现在时间分配上,而不是某个数字配置项。
-
default只适合低优路径,且建议配合计数器或退避,防止低优长期霸占循环 - 高优路径里加
default是危险操作,等于主动放弃确定性 - 纯
select+default轮询 CPU 占用高,不如用定时器节拍控制节奏
container/heap 实现的任务队列比多 channel 更适合复杂优先级场景
当优先级不止两级、需要动态排序(比如按提交时间 + 优先级组合)、或要求严格 FIFO+优先级混合时,container/heap 比多个 channel 更可控。它能构建最小堆,让 priority 小的先出,还能在 Less 方法里加时间戳二次排序,避免同优先级任务饥饿。
但要注意:heap 本身不是并发安全的,所有 Push/Pop 操作必须用 sync.Mutex 保护;而且它不支持运行中修改已有任务的优先级 —— 想提升某个待执行任务的优先级,只能标记原任务取消,新建一个更高优的新任务重新入堆。
- 别用
sync.Pool替代任务队列,它只管对象复用,不负责调度或排序 - 关闭任务池时,别
close通道来通知停止 —— 优先队列是内存结构,关通道没意义,还可能 panic - 优雅退出要靠
context.Context驱动:主循环每次Pop前检查ctx.Err() != nil,然后wg.Wait()等协程自然结束


















