业务代码里到处都是排序
文章按发布时间倒序,订单按金额排序,排行榜按分数排序,后台列表按状态和创建时间排序。排序看起来是基础算法,但在业务代码里非常常见。Go 标准库的 sort 包提供了常用能力:排序字符串、整数、浮点数,按自定义规则排序结构体切片,做稳定排序和二分搜索。
入门阶段你不需要手写排序算法。更重要的是理解如何表达排序规则,以及排序是否会改变原切片。很多 bug 不是算法错,而是“我以为这个列表还是原顺序”,结果排序函数已经原地修改了它。
这篇文章用文章列表、订单列表和分数列表做例子,讲清楚 Go 的排序和搜索常见写法。
基本类型排序
字符串排序:
names := []string{"Charlie", "Alice", "Bob"}
sort.Strings(names)
fmt.Println(names)
整数排序:
scores := []int{88, 95, 72}
sort.Ints(scores)
fmt.Println(scores)
降序可以用 sort.Slice:
sort.Slice(scores, func(i, j int) bool {
return scores[i] > scores[j]
})
注意这些排序都会原地修改切片。如果你要保留原顺序,先复制:
sorted := make([]int, len(scores))
copy(sorted, scores)
sort.Ints(sorted)
切片共享底层数组,直接赋值不是复制:
sorted := scores
这只是让两个切片指向同一个底层数组,排序 sorted 也会影响 scores。
排序结构体切片
定义文章:
type Article struct {
ID int64
Title string
CreatedAt time.Time
}
按创建时间倒序:
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].Title < articles[j].Title
})
排序规则函数返回 true 表示 i 应该排在 j 前面。这个函数不要写得太复杂。复杂排序最好提成有名字的函数:
func newerFirst(a, b Article) bool {
return a.CreatedAt.After(b.CreatedAt)
}
sort.Slice(articles, func(i, j int) bool {
return newerFirst(articles[i], articles[j])
})
这样测试排序规则也更方便。
多字段排序
订单先按状态,再按创建时间:
type Order struct {
ID int64
Status string
CreatedAt time.Time
}
规则:
func statusRank(status string) int {
switch status {
case "pending":
return 1
case "paid":
return 2
case "closed":
return 3
default:
return 99
}
}
排序:
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.SliceStable。
例如文章列表原本已经按发布时间倒序排好,现在只想把置顶文章排到前面,同时保持置顶内部和非置顶内部原顺序:
type Article struct {
Title string
Pinned bool
}
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)
}
Search 找到第一个让函数返回 true 的位置。前提是这个判断在有序数据上从 false 变成 true。它比手写二分更不容易出错,但初学时要多读几遍。
查找插入位置:
insertAt := sort.Search(len(nums), func(i int) bool {
return nums[i] >= 6
})
fmt.Println(insertAt)
如果要频繁查找,先排序再二分很有用。如果只查一次,直接遍历可能更简单。
小结
Go 的 sort 包覆盖了大多数业务排序需求。基本类型有 sort.Strings、sort.Ints,结构体切片用 sort.Slice,需要保持相等元素原顺序时用 sort.SliceStable,有序列表查找用 sort.Search。
排序会原地修改切片,保留原顺序前要复制。排序规则要清楚,复杂规则拆成函数。多字段排序和稳定排序是业务列表最常见的两个细节。
不要为了展示算法能力手写排序。标准库已经足够可靠,把精力放在表达正确业务顺序上,才是 Go 项目里更常见也更重要的工作。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。