Go 排序与搜索集合完全指南:sort 包、container 包与泛型 slices 对比

全面讲解 Go sort 包的排序与搜索能力,深入 sort.Slice、SliceStable、Search 的使用,详解 container/heap、list、ring 的实现与应用场景,对比 Go 1.21+ 泛型 slices 包的 Sort、SortFunc 新特性。

业务代码里到处都是排序

文章按发布时间倒序排列,订单按金额排序,排行榜按分数排序,后台列表按状态和创建时间排序。排序看起来是基础算法,但在业务代码中出现的频率极高。Go 标准库的 sort 包提供了覆盖绝大多数需求的排序能力,而更进一步的 container 包则提供了堆、双向链表和循环缓冲区等高级数据结构。

入门阶段你不需要手写排序算法,标准库的实现经过高度优化。更重要的是理解如何表达排序规则,以及排序是否会改变原切片。很多 bug 的根源不是算法错误,而是开发者没有意识到排序函数已经原地修改了原始数据。

这篇文章从基本排序出发,逐步深入到二分搜索、稳定排序、多字段排序,再到 container 包的实战应用,最后对比 Go 1.21+ 引入的泛型 slices 包新特性,帮助你建立完整的排序与集合知识体系。

基本类型排序

字符串排序

package main

import (
    "fmt"
    "sort"
)

func main() {
    names := []string{"Charlie", "Alice", "Bob", "alice", "ALICE"}
    sort.Strings(names)
    fmt.Println(names)
    // 输出:[ALICE Alice Bob Charlie alice]
}

sort.Strings 按照字节值升序排列。如果需要不区分大小写的排序,需要自定义比较函数:

names := []string{"Charlie", "Alice", "Bob", "alice", "ALICE"}
sort.Slice(names, func(i, j int) bool {
    return strings.ToLower(names[i]) < strings.ToLower(names[j])
})
// 输出:[Alice alice ALICE Bob Charlie]

整数与浮点数排序

scores := []int{88, 95, 72, 100, 60}
sort.Ints(scores)
fmt.Println(scores) // [60 72 88 95 100]

temps := []float64{23.5, 19.2, 25.1, 18.0}
sort.Float64s(temps)
fmt.Println(temps) // [18 19.2 23.5 25.1]

降序排序

Go 的 sort 包没有专门的降序函数,使用 sort.Slice 配合反向比较:

scores := []int{88, 95, 72, 100}
sort.Slice(scores, func(i, j int) bool {
    return scores[i] > scores[j]
})
fmt.Println(scores) // [100 95 88 72]

原地修改与复制

所有 sort 包函数都会原地修改切片。如果需要保留原始顺序:

original := []int{3, 1, 4, 1, 5}
sorted := make([]int, len(original))
copy(sorted, original)
sort.Ints(sorted)

fmt.Println(original) // [3 1 4 1 5]
fmt.Println(sorted)   // [1 1 3 4 5]

一个常见的错误是直接赋值:

sorted := original // 错误!这只是让两个切片指向同一个底层数组
sort.Ints(sorted)  // original 也被排序了

结构体切片排序

单字段排序

type Article struct {
    ID        int64
    Title     string
    CreatedAt time.Time
    Views     int
}

// 按创建时间倒序
sort.Slice(articles, func(i, j int) bool {
    return articles[i].CreatedAt.After(articles[j].CreatedAt)
})

// 按浏览量降序
sort.Slice(articles, func(i, j int) bool {
    return articles[i].Views > articles[j].Views
})

将排序规则提取为函数

func newerFirst(a, b Article) bool {
    return a.CreatedAt.After(b.CreatedAt)
}

func byViewsDesc(a, b Article) bool {
    return a.Views > b.Views
}

sort.Slice(articles, func(i, j int) bool {
    return newerFirst(articles[i], articles[j])
})

提取排序规则的好处是便于测试:你可以直接单元测试 newerFirst 函数,而不需要构造完整的切片。

多字段排序

订单先按状态,再按创建时间

type Order struct {
    ID        int64
    Status    string    // pending, paid, shipped, closed
    CreatedAt time.Time
    Amount    float64
}

func statusRank(status string) int {
    switch status {
    case "pending":
        return 1
    case "paid":
        return 2
    case "shipped":
        return 3
    case "closed":
        return 4
    default:
        return 99
    }
}

func sortOrders(orders []Order) {
    sort.Slice(orders, func(i, j int) bool {
        leftRank := statusRank(orders[i].Status)
        rightRank := statusRank(orders[j].Status)
        if leftRank != rightRank {
            return leftRank < rightRank
        }
        return orders[i].CreatedAt.After(orders[j].CreatedAt)
    })
}

三字段排序:状态 > 金额降序 > 创建时间

sort.Slice(orders, func(i, j int) bool {
    // 第一优先级:状态
    leftRank := statusRank(orders[i].Status)
    rightRank := statusRank(orders[j].Status)
    if leftRank != rightRank {
        return leftRank < rightRank
    }

    // 第二优先级:金额降序
    if orders[i].Amount != orders[j].Amount {
        return orders[i].Amount > orders[j].Amount
    }

    // 第三优先级:创建时间倒序
    return orders[i].CreatedAt.After(orders[j].CreatedAt)
})

多字段排序的规则要写得明确。不要把所有条件压成一行,否则后续维护会很困难。

稳定排序

普通排序不保证相等元素保持原有相对顺序。如果需要保持原有顺序,使用 sort.SliceStable。

type Article struct {
    Title  string
    Author string
    Pinned bool
}

// 先把置顶文章排到前面,同时保持内部原有顺序
sort.SliceStable(articles, func(i, j int) bool {
    return articles[i].Pinned && !articles[j].Pinned
})

稳定排序的成本略高于普通排序,时间复杂度为 O(n log² n) 或 O(n log n)(取决于实现),而普通排序是 O(n log n)。但对于需要保持相对顺序的场景,这是必要的代价。

另一个稳定排序的典型应用是分层排序:先按次要条件排序,再按主要条件稳定排序:

// 先按作者排序
sort.SliceStable(articles, func(i, j int) bool {
    return articles[i].Author < articles[j].Author
})

// 再稳定按置顶排序,置顶文章排在前面,
// 同时保持同置顶状态内部按作者的顺序
sort.SliceStable(articles, func(i, j int) bool {
    return articles[i].Pinned && !articles[j].Pinned
})

二分搜索

sort.Search 基本用法

nums := []int{1, 3, 5, 7, 9}
target := 5

index := sort.Search(len(nums), func(i int) bool {
    return nums[i] >= target
})

if index < len(nums) && nums[index] == target {
    fmt.Println("found at", index)
} else {
    fmt.Println("not found, would insert at", index)
}

sort.Search 找到第一个让函数返回 true 的位置。前提是该判断在有序数据上呈现从 false 到 true 的单调变化。

查找插入位置

nums := []int{1, 3, 5, 7, 9}
insertAt := sort.Search(len(nums), func(i int) bool {
    return nums[i] >= 6
})
fmt.Println(insertAt) // 4,即在 7 的位置插入 6

结构体切片搜索

type User struct {
    ID   int64
    Name string
}

users := []User{
    {ID: 1, Name: "Alice"},
    {ID: 3, Name: "Bob"},
    {ID: 5, Name: "Charlie"},
}

index := sort.Search(len(users), func(i int) bool {
    return users[i].ID >= 3
})

SearchInts、SearchStrings、SearchFloat64s

对于基本类型切片,sort 包提供了简化的搜索函数:

nums := []int{10, 20, 30, 40, 50}
index := sort.SearchInts(nums, 30)
// index = 2

container/heap:优先队列

Go 的 container/heap 包实现了通用的堆数据结构。要使用该包,需要实现 heap.Interface。

最小堆实现

package main

import (
    "container/heap"
    "fmt"
)

type Item struct {
    Value    string
    Priority int
    Index    int // 在堆中的索引,必须更新
}

type PriorityQueue []*Item

func (pq PriorityQueue) Len() int { return len(pq) }

func (pq PriorityQueue) Less(i, j int) bool {
    return pq[i].Priority < pq[j].Priority
}

func (pq PriorityQueue) Swap(i, j int) {
    pq[i], pq[j] = pq[j], pq[i]
    pq[i].Index = i
    pq[j].Index = j
}

func (pq *PriorityQueue) Push(x interface{}) {
    n := len(*pq)
    item := x.(*Item)
    item.Index = n
    *pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() interface{} {
    old := *pq
    n := len(old)
    item := old[n-1]
    old[n-1] = nil
    item.Index = -1
    *pq = old[:n-1]
    return item
}

func main() {
    pq := make(PriorityQueue, 0)
    heap.Init(&pq)

    heap.Push(&pq, &Item{Value: "task1", Priority: 3})
    heap.Push(&pq, &Item{Value: "task2", Priority: 1})
    heap.Push(&pq, &Item{Value: "task3", Priority: 2})

    for pq.Len() > 0 {
        item := heap.Pop(&pq).(*Item)
        fmt.Printf("%s (priority %d)\n", item.Value, item.Priority)
    }
}

堆的典型应用场景

  1. 调度器:按优先级处理任务
  2. 合并 K 个有序列表
  3. Top K 问题
  4. 事件驱动系统的时间队列

container/list:双向链表

container/list 提供了标准双向链表实现:

package main

import (
    "container/list"
    "fmt"
)

func main() {
    l := list.New()

    l.PushBack("first")
    l.PushBack("second")
    l.PushFront("zero")

    for e := l.Front(); e != nil; e = e.Next() {
        fmt.Println(e.Value)
    }
}

双向链表的适用场景:

  • 需要频繁在中间插入或删除元素
  • 实现 LRU 缓存
  • 需要 O(1) 删除已知节点

但要注意:链表随机访问是 O(n),缓存局部性比切片差,在遍历时通常比切片慢。除非明确需要 O(1) 中间删除,否则优先考虑切片。

LRU 缓存实现

type LRUCache struct {
    capacity int
    cache    map[string]*list.Element
    order    *list.List
}

type entry struct {
    key   string
    value string
}

func NewLRUCache(capacity int) *LRUCache {
    return &LRUCache{
        capacity: capacity,
        cache:    make(map[string]*list.Element),
        order:    list.New(),
    }
}

func (c *LRUCache) Get(key string) (string, bool) {
    if elem, ok := c.cache[key]; ok {
        c.order.MoveToFront(elem)
        return elem.Value.(*entry).value, true
    }
    return "", false
}

func (c *LRUCache) Put(key string, value string) {
    if elem, ok := c.cache[key]; ok {
        elem.Value.(*entry).value = value
        c.order.MoveToFront(elem)
        return
    }

    if len(c.cache) >= c.capacity {
        oldest := c.order.Back()
        if oldest != nil {
            c.order.Remove(oldest)
            delete(c.cache, oldest.Value.(*entry).key)
        }
    }

    elem := c.order.PushFront(&entry{key: key, value: value})
    c.cache[key] = elem
}

container/ring:循环缓冲区

container/ring 提供了固定长度的循环链表:

package main

import (
    "container/ring"
    "fmt"
)

func main() {
    r := ring.New(5)

    for i := 0; i < r.Len(); i++ {
        r.Value = i
        r = r.Next()
    }

    r.Do(func(p interface{}) {
        fmt.Println(p)
    })
}

循环缓冲区适合固定大小的轮询场景,如负载均衡器中的服务器列表、固定大小的日志缓冲等。

Go 1.21+ 泛型 slices 包

Go 1.21 引入了 slices 和 maps 泛型包,提供了类型安全的排序和搜索函数。

slices.Sort 与 slices.SortFunc

import "slices"

nums := []int{3, 1, 4, 1, 5, 9}
slices.Sort(nums)
fmt.Println(nums) // [1 1 3 4 5 9]

对结构体切片排序:

users := []User{
    {Name: "Bob", Age: 30},
    {Name: "Alice", Age: 25},
    {Name: "Charlie", Age: 35},
}

// 按年龄升序
slices.SortFunc(users, func(a, b User) int {
    return cmp.Compare(a.Age, b.Age)
})

// 按名称升序
slices.SortFunc(users, func(a, b User) int {
    return cmp.Compare(a.Name, b.Name)
})

slices.BinarySearch

nums := []int{10, 20, 30, 40, 50}
idx, found := slices.BinarySearch(nums, 30)
// idx = 2, found = true

idx, found = slices.BinarySearch(nums, 25)
// idx = 2, found = false

泛型与传统对比

特性sort 包slices 包
类型安全运行时报错编译期检查
易用性需要 interface直接操作切片
性能内联优化好泛型实例化后同等
兼容性所有 Go 版本Go 1.21+

新项目优先使用 slices 包,旧代码可以逐步迁移。

常见错误与最佳实践

错误 1:sort 自定义函数不稳定

// 错误:不满足严格弱序(strict weak ordering)
sort.Slice(items, func(i, j int) bool {
    return items[i].Score <= items[j].Score // <= 不是 <
})

// 正确
sort.Slice(items, func(i, j int) bool {
    return items[i].Score < items[j].Score // 使用 <
})

错误 2:在遍历时排序

// 危险:遍历中修改切片
for i := range items {
    sort.Slice(items, ...) // 可能导致循环变量错乱
}

错误 3:忽略 sort.Search 的 not-found 情况

index := sort.Search(len(nums), func(i int) bool {
    return nums[i] >= target
})
// 必须检查 index < len(nums) && nums[index] == target

最佳实践

  1. 排序规则函数使用 < 而不是 <=,满足严格弱序
  2. 复杂排序规则提取为独立函数,方便测试
  3. 需要保持相等元素顺序时使用 SliceStable
  4. 二分搜索前确保数据已排序
  5. 优先使用 Go 1.21+ 的 slices 包获得编译期类型安全

小结

Go 的 sort 包覆盖了绝大多数业务排序需求。基本类型用 sort.Ints、sort.Strings,结构体切片用 sort.Slice,需要保持相对顺序时用 sort.SliceStable,有序数据查找用 sort.Search。排序会原地修改切片,保留原始顺序前记得复制。

container 包提供了堆、双向链表和循环缓冲区三种高级数据结构。堆适合优先队列,链表适合频繁中间插入删除,环适合轮询场景。

Go 1.21+ 引入的泛型 slices 包提供了类型安全的替代方案。slices.Sort、slices.SortFunc 和 slices.BinarySearch 在新项目中更值得优先使用。

不要为了展示自己的算法能力而手写排序。标准库已经足够可靠,把精力放在表达正确的业务顺序上,才是 Go 项目里更常见也更有价值的工作。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「golang」更多文章

  1. 《Go 语言编程实战》目录
  2. 《Go 语言编程实战》18.3 上线、观测与迭代
  3. 《Go 语言编程实战》18.2 故障演练