
Go 里没有内置带权重的并发任务调度器
标准库 sync.WaitGroup 和 chan 不管优先级,context.WithTimeout 也只管取消不管排队顺序。真要按权重跑任务,得自己搭一层“带权分发器”——不是改 goroutine 启动逻辑,而是控制任务进队、出队、执行的节奏。
常见错误是直接在 go f() 前加 if 判断权重,结果只是跳过启动,没解决高权重任务被低权重任务挤占 channel 缓冲区或 worker 队列的问题。
核心思路:用最小堆(container/heap)实现优先级队列,把任务封装成带 priority 字段的结构体,再配一个固定数量的 worker goroutine 持续从堆里取最高优任务执行。
用 container/heap 实现可排序的任务队列
Go 官方堆包不直接支持泛型比较,必须为任务类型实现 heap.Interface 的五个方法。最容易踩的坑是 Less(i, j int) bool 写反(比如写成小根堆逻辑却想做高优先执行),导致 heap.Pop 总拿错任务。
立即学习“go语言免费学习笔记(深入)”;
- 权重字段建议用
int64类型,避免浮点精度问题和负权歧义 -
Push时别直接append到切片末尾再heap.Init,要用heap.Push(&q, task)触发自动上浮 - 如果多个任务权重相同,需在
Less中加入时间戳或自增 ID 作为第二排序键,否则出队顺序不确定
示例关键片段:
type Task struct {
Fn func()
Priority int64
ID int64 // 用于同权时保序
}
func (t Task) Less(other Task) bool {
if t.Priority != other.Priority {
return t.Priority > other.Priority // 注意:> 表示高权先出
}
return t.ID < other.ID
}worker 启动后如何安全消费带权队列
不能让每个 worker 都去 heap.Pop,会竞态;也不能用普通 channel 中转,因为 channel 无法按优先级排序。正确做法是:单个 goroutine 独占堆,负责 Pop + 发送到无缓冲 channel,worker 从该 channel 收任务。
- 堆操作全程加
sync.Mutex,哪怕只读也要锁——heap.Pop会修改底层切片长度 - channel 用无缓冲或小缓冲(如 1),避免高权任务在 channel 里被低权任务“插队”等待
- worker 里执行
task.Fn()时务必 recover panic,否则一个崩溃会让整个 worker 退出,剩余任务卡死 - 别在
task.Fn里直接调runtime.Goexit或长阻塞,这等于占用 worker 不放回池
权重值设计不当会导致饥饿或误判
权重不是越大越好。如果用时间戳做权(如 UnixNano),数值过大易溢出;如果用业务等级硬编码(如 1=低,10=高),扩展性差且难调试。
- 推荐用对数尺度:HTTP 请求按响应时间预期分级,
priority = 1000 - int64(math.Log(float64(expectedMs)) * 100),保证数值稳定、可读、不易溢出 - 避免零或负权重:某些堆实现对
- 权重变更只能发生在入队前;已入堆的任务无法动态调整优先级——要改就得重新 Push,旧实例靠标识位丢弃
真正麻烦的是跨服务权重对齐:A 服务发来的 “高优” 任务,在 B 服务眼里可能只是中等。这事没法靠代码自动解,得靠协议约定或配置中心同步权重映射表。










