业务代码里到处都是排序
文章按发布时间倒序排列,订单按金额排序,排行榜按分数排序,后台列表按状态和创建时间排序。排序看起来是基础算法,但在业务代码中出现的频率极高。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)
}
}
堆的典型应用场景
- 调度器:按优先级处理任务
- 合并 K 个有序列表
- Top K 问题
- 事件驱动系统的时间队列
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
最佳实践
- 排序规则函数使用
<而不是<=,满足严格弱序 - 复杂排序规则提取为独立函数,方便测试
- 需要保持相等元素顺序时使用
SliceStable - 二分搜索前确保数据已排序
- 优先使用 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 项目里更常见也更有价值的工作。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。