Go 怎么用 heap 实现按优先级领取任务
如果任务不是“先来先服务”,而是要优先领取紧急任务,普通切片配合遍历查找会越来越笨重。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。
PHP array_filter 后 JSON 为什么变成对象
- 上一篇
- PHP array_filter 后 JSON 为什么变成对象
- 下一篇
- gfx工具箱打不开怎么排查?关闭游戏、版本选择与发热提醒
-
- Golang · Go教程 | 1天前 | 并发 · Go教程 · trace.NewTask go tool trace Go runtime/trace 执行跟踪 trace.WithRegion
- Go runtime/trace 怎样按任务标记跟踪区间
- 391浏览 收藏
-
- Golang · Go教程 | 1天前 | flag · go · Go 命令行参数 flag.TextVar encoding.TextUnmarshaler
- Go flag.TextVar 怎样把自定义类型接入命令行
- 184浏览 收藏
-
- Golang · Go教程 | 1天前 | 错误处理 · 模板 · Go教程 · html/template · 模板渲染 Go html/template missingkey 模板缺失字段 Execute错误
- Go html/template 缺失字段的严格渲染配置与错误处理
- 371浏览 收藏
-
- Golang · Go教程 | 1天前 | html · go · 模板 · url · Go html/template 自动转义 URL上下文 template.URL
- Go html/template 怎样避免 URL 上下文误用
- 326浏览 收藏
-
- Golang · Go教程 | 1天前 |
- Go text/template.ParseFS 怎样加载嵌入文件模板
- 208浏览 收藏
-
- Golang · Go教程 | 1天前 |
- Go database/sql.NamedArg 怎样给 SQL 参数命名
- 311浏览 收藏
-
- Golang · Go教程 | 1天前 | HTTP · go · Gin · Gin ShouldBindBodyWith 请求体重复绑定 ShouldBind binding.JSON
- Gin ShouldBindBodyWith 怎么让同一个请求体绑定两次
- 134浏览 收藏
-
- Golang · Go教程 | 1天前 | go · 文件系统 · Go os.ReadDir Go File.ReadDir Go DirEntry Go 目录遍历
- Go os.ReadDir 怎样高效读取目录条目
- 269浏览 收藏
-
- Golang · Go教程 | 1天前 | go · Go go/types.Config Go Importer Go 类型检查 types.Check
- Go go/types.Config 怎样给类型检查接入导入器
- 222浏览 收藏
-
- Golang · Go教程 | 1天前 |
- Go maphash.Hash 怎样用固定种子复现哈希结果
- 341浏览 收藏
-
- Golang · Go教程 | 1天前 | Go 字节切片 encoding/binary AppendByteOrder
- Go binary.ByteOrder.Append 怎样把整数追加到现有字节切片
- 373浏览 收藏
-
- Golang · Go教程 | 1天前 | go · IPv6 Go ipv4 netip.ParsePrefix CIDR
- Go netip.ParsePrefix 怎样解析 CIDR 字符串
- 334浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 579次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 619次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 650次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 615次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 431次使用
-
- Golang迭代如何在Go中循环数据结构使用详解
- 2022-12-22 148浏览
-
- 详解如何在Go语言中循环数据结构
- 2022-12-22 406浏览
-
- GScript 编写标准库示例详解
- 2022-12-30 369浏览
-
- 关于Golang标准库flag的全面讲解
- 2023-02-25 344浏览
-
- Go语言数据结构之双链表学习教程
- 2022-12-30 280浏览

