登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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
}
Go container heap 优先队列中 Task、PriorityQueue、Less、Swap、Push 和 Pop 的静态关系
图1:PriorityQueue 通过 Task、Less、Swap、Push 和 Pop 组成可维护的优先队列结构。

这里的 PushPop 是给 container/heap 内部调用的接口方法;业务代码新增和领取任务时,应调用包级函数 heap.Pushheap.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.InitO(n)让已有切片满足堆约束
新增任务heap.PushO(log n)插入并恢复顺序
领取任务heap.PopO(log n)取出当前最高优先级任务
调整任务heap.FixO(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,不要手动从切片中删除后继续使用旧索引。

Go heap.Fix 调整任务优先级时的 Task index、PriorityQueue 和根节点关系
图2:修改 Priority 后,借助 index 定位任务并由 heap.Fix 恢复优先队列的静态边界。

把示例接入业务时检查这几个边界

第一,优先级相同的任务不自动保证稳定顺序;如果业务要求先进先出,可以再保存递增序号,在 Less 中用序号做第二排序条件。第二,队列通常应由一个拥有者串行修改;若多个 goroutine 同时 Push、Pop 或 Update,需要在外层加锁,container/heap 本身不提供并发保护。第三,空队列取任务前先判断 Len,不要假设 heap.Pop 会返回空值。

常见问题

为什么 Less 写成大于号却叫最小堆?

因为 heap 包按 Less 判断“更小”,用大于号后,业务上的高优先级在比较关系中反而更小,于是会先到根节点。

能不能直接对 PriorityQueue 排序后取任务?

可以排序,但每次新增、领取都要重新排序;优先队列只维护局部堆结构,更适合持续插入和取出。

什么时候用 heap.Init,什么时候用 heap.Push?

已有一批任务时先用 heap.Init,单个任务进入运行中的队列时用 heap.Push

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>