Go 怎么用 heap 实现按优先级领取任务
来源:17golang原创
时间:2026-09-06 06:34:51 397浏览 收藏
如果任务不是“先来先服务”,而是要优先领取紧急任务,普通切片配合遍历查找会越来越笨重。Go 标准库的 container/heap 可以把一组任务组织成优先队列:把优先级高的任务定义为“更小”,heap.Pop 就会先返回它。
实现重点只有三件事:Less用大于号实现高优先级优先,修改任务优先级后调用heap.Fix,以及用指针接收者维护队列长度和索引。
heap.Interface的默认语义是最小堆,根元素位于索引 0。- 队列新增和领取都是 O(log n),批量建堆可用
heap.Init。 - 直接改优先级不会自动调整位置,必须调用
heap.Fix。
先把任务模型变成可调整的优先队列
下面的任务包含名称、优先级和当前索引。索引不是业务字段,而是为了让优先级变化时能快速定位元素。Swap 交换任务后必须同步两个索引,否则后续 heap.Fix 可能修错位置。
package main
import (
"container/heap"
"fmt"
)
type Task struct {
Name string
Priority int
index int // 记录任务当前位于堆切片的哪个位置
}
type PriorityQueue []*Task
func (q PriorityQueue) Len() int { return len(q) }
func (q PriorityQueue) Less(i, j int) bool {
// heap 是最小堆;用大于号即可让数值更大的任务先出队。
return q[i].Priority > q[j].Priority
}
func (q PriorityQueue) Swap(i, j int) {
q[i], q[j] = q[j], q[i]
q[i].index = i // 交换后同步新位置
q[j].index = j
}
func (q *PriorityQueue) Push(x any) {
task := x.(*Task)
task.index = len(*q) // 新元素先记录在追加位置
*q = append(*q, task)
}
func (q *PriorityQueue) Pop() any {
old := *q
n := len(old)
task := old[n-1]
old[n-1] = nil // 解除引用,避免队列缩短后继续占住任务
task.index = -1
*q = old[:n-1]
return task
}

这里的 Push 和 Pop 是给 container/heap 内部调用的接口方法;业务代码新增和领取任务时,应调用包级函数 heap.Push、heap.Pop,不要直接调用队列方法。
用 heap.Init、heap.Push 和 heap.Pop 领取任务
已有任务先组成切片,再调用一次 heap.Init 建立堆不变量。以后新增任务使用 heap.Push,领取任务使用 heap.Pop。因为 Less 采用大于号,输出顺序会从高优先级到低优先级。
func main() {
queue := PriorityQueue{
&Task{Name: "生成日报", Priority: 2},
&Task{Name: "修复支付回调", Priority: 9},
&Task{Name: "刷新缓存", Priority: 5},
}
heap.Init(&queue) // 批量建堆,复杂度 O(n)
heap.Push(&queue, &Task{Name: "处理告警", Priority: 8})
for queue.Len() > 0 {
task := heap.Pop(&queue).(*Task)
fmt.Printf("%d - %s\n", task.Priority, task.Name)
}
}
这段程序的领取次序是 9、8、5、2。注意堆只保证根节点是当前最优元素,不保证底层切片整体已经排好序,所以不能把 queue 当成普通排序结果直接遍历。
| 动作 | 调用 | 复杂度 | 用途 |
|---|---|---|---|
| 批量初始化 | heap.Init | O(n) | 让已有切片满足堆约束 |
| 新增任务 | heap.Push | O(log n) | 插入并恢复顺序 |
| 领取任务 | heap.Pop | O(log n) | 取出当前最高优先级任务 |
| 调整任务 | heap.Fix | O(log n) | 修改优先级后重新定位 |
任务优先级变化后为什么要调用 heap.Fix
例如一个普通任务突然升级为紧急任务,直接修改 task.Priority 只改变了字段,不会通知堆重新调整。可以给队列增加一个更新方法,先写入新值,再用任务保存的 index 调用 heap.Fix。
func (q *PriorityQueue) Update(task *Task, priority int) {
if task.index = len(*q) {
return // 任务已出队或索引无效,不再修改堆
}
task.Priority = priority
heap.Fix(q, task.index) // 只调整受影响的路径
}
如果任务已经被 heap.Pop 取走,示例中的 index 会变成 -1,更新方法可以直接返回。若要删除任意位置的任务,则使用 heap.Remove,不要手动从切片中删除后继续使用旧索引。

把示例接入业务时检查这几个边界
第一,优先级相同的任务不自动保证稳定顺序;如果业务要求先进先出,可以再保存递增序号,在 Less 中用序号做第二排序条件。第二,队列通常应由一个拥有者串行修改;若多个 goroutine 同时 Push、Pop 或 Update,需要在外层加锁,container/heap 本身不提供并发保护。第三,空队列取任务前先判断 Len,不要假设 heap.Pop 会返回空值。
常见问题
为什么 Less 写成大于号却叫最小堆?
因为 heap 包按 Less 判断“更小”,用大于号后,业务上的高优先级在比较关系中反而更小,于是会先到根节点。
能不能直接对 PriorityQueue 排序后取任务?
可以排序,但每次新增、领取都要重新排序;优先队列只维护局部堆结构,更适合持续插入和取出。
什么时候用 heap.Init,什么时候用 heap.Push?
已有一批任务时先用 heap.Init,单个任务进入运行中的队列时用 heap.Push。
-
148 收藏
-
406 收藏
-
369 收藏
-
344 收藏
-
280 收藏
-
366 收藏
-
321 收藏
-
110 收藏
-
473 收藏
-
288 收藏
-
233 收藏
-
485 收藏
-
351 收藏
-
115 收藏
-
192 收藏
-
285 收藏
-
322 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习