9.2 slices/maps/cmp 标准库
上一节我们学会了写泛型。但绝大多数时候,你不需要自己写泛型函数——标准库已经把最常用的都写好了,而且写得比手写更稳。slices、maps、cmp 这三个包是 Go 1.21 转正的泛型标准库,它们替代了无数「自己写个 for 循环」的场景。这一节把它们和 TaskAPI 的查询逻辑接起来。
本节给 TaskAPI 的列表查询做重构:用
slices.SortFunc按标题排序、用slices.DeleteFunc过滤已完成任务、用maps.Keys把存储索引摊成有序切片,最后交给上一节的Page[task.Task]分页。这些函数都基于泛型,正好把第 9 章的两节串成一条线。
9.2.1 三个包的分工
先建立全局印象:
| 包 | 作用对象 | 典型用途 |
|---|---|---|
slices | 切片 []E | 排序、查找、删除、去重、比较 |
maps | 映射 map[K]V | 取键/值、克隆、比较、批量删 |
cmp | 可比较/可排序值 | 比较、取小、返回首个非零值 |
它们全部是泛型 API,调用时类型由实参推断,和上一节的 Sum、Max 一样自然。一个心智模型:「我要对切片/映射做点什么」——先查这三个包,再考虑自己写循环。
9.2.2 slices 速查表
slices 的函数很多,按用途归类:
| 类别 | 函数 | 说明 |
|---|---|---|
| 排序 | Sort、SortFunc、SortStableFunc、IsSorted | 就地排序,Func 版自定义比较 |
| 查找 | Contains、Index、IndexFunc、BinarySearch | 返回是否包含 / 下标 |
| 删除 | Delete、DeleteFunc | 就地删除并返回新切片 |
| 去重 | Compact、CompactFunc | 去掉相邻重复元素 |
| 增改 | Insert、Replace、Clone、Grow | 插入、替换、克隆、预分配 |
| 极值 | Min、Max、MinFunc、MaxFunc | 最小/最大元素 |
| 迭代 | All、Backward、Sorted、Collect | 与 iter.Seq 互转 |
两个最容易误用的点先埋在这里:Compact 只去相邻重复(要先排序才能全量去重),Delete*/Compact/Insert 都是原地修改(返回的切片要重新赋值)。
9.2.3 maps 速查表
maps 包比 slices 小得多:
| 函数 | 说明 |
|---|---|
Keys、Values | 返回 iter.Seq,可配合 slices.Sorted 转有序切片 |
Clone | 浅拷贝一个映射 |
Copy | 把 src 的键值覆盖进 dst |
Equal、EqualFunc | 比较两个映射是否相等 |
DeleteFunc | 按条件删除键值对 |
Insert | 把 iter.Seq2 的键值对插入映射 |
Keys/Values 返回的是迭代器而非切片,这是刻意的设计——避免为「只想遍历」的调用方多分配一个切片。需要切片时再显式收集。
9.2.4 cmp:比较的通用工具
cmp 只有三个函数,但出场率极高:
func Compare[T cmp.Ordered](x, y T) int // x<y 返回负,相等返回 0,x>y 返回正
func Less[T cmp.Ordered](x, y T) bool // x < y
func Or[T comparable](vals ...T) T // 返回第一个非零值
cmp.Compare 的返回值约定(负/零/正)正好是 slices.SortFunc 期望的比较函数签名,所以两者是天生一对:
slices.SortFunc(tasks, func(a, b task.Task) int {
return cmp.Compare(a.Title, b.Title)
})
cmp.Or 是「兜底值」的利器:
fmt.Println(cmp.Or("", "", "fallback")) // fallback
它常用来给配置项填默认值——第一个非零参数胜出。
9.2.5 重写查询:按标题排序后分页
把 TaskAPI 的「列出任务」改成「按标题排序、分页返回」:
package main
import (
"cmp"
"fmt"
"slices"
"taskapi/internal/pagination"
"taskapi/internal/task"
)
func main() {
tasks := []task.Task{
task.New(1, "写第 9 章"),
task.New(2, "复习 slices"),
task.New(3, "补充测试"),
}
slices.SortFunc(tasks, func(a, b task.Task) int {
return cmp.Compare(a.Title, b.Title)
})
p := pagination.New(tasks, 1, 2)
fmt.Printf("total=%d page=%d size=%d items=%d\n", p.Total, p.Number, p.Size, len(p.Items))
for _, t := range p.Items {
fmt.Println("-", t.Title)
}
}
实测输出:
total=3 page=1 size=2 items=2
- 写第 9 章
- 复习 slices
注意排序是按 UTF-8 字节序比较的:写、复、补 的 UTF-8 编码依次递增,所以结果是「写第 9 章」在前。对中文排序,cmp.Compare 给的是确定但未必符合人类语感的顺序——要做拼音排序得引入额外依赖,卷一不做。
9.2.6 过滤与去重
实际业务里更常见的是「筛掉已完成的」「去掉重复标题」。用 slices.DeleteFunc 与 slices.Compact:
nums := []int{1, 2, 3, 4, 5}
odd := slices.DeleteFunc(slices.Clone(nums), func(n int) bool { return n%2 == 0 })
fmt.Println("odd:", odd) // [1 3 5]
uniq := slices.Compact(slices.Clone([]string{"a", "a", "b"}))
fmt.Println("compact:", uniq) // [a b]
这里有两个必须记住的坑:
DeleteFunc会原地修改底层数组。上例先slices.Clone出一份副本再删,就是为了不破坏nums。如果nums之后还要用,一定要先克隆。Compact只去相邻重复。[]string{"a", "b", "a"}经Compact后仍是三个元素。要全量去重,先Sort再Compact,或者用 map 手工去重。
9.2.7 从 map 到有序列表
MemStore 内部用 map[int64]task.Task 存任务,遍历顺序是随机的。要输出稳定顺序,就得把 map 转成排序后的切片:
m := map[string]int{"a": 1, "b": 2}
keys := slices.Sorted(maps.Keys(m))
fmt.Println("keys:", keys) // [a b]
maps.Keys(m) 返回 iter.Seq[string],slices.Sorted 把它收集成切片并排序——一行完成「取键 + 排序」。如果还要按 value 排序,就得自己收集再 SortFunc:
type kv struct {
K string
V int
}
pairs := make([]kv, 0, len(m))
for k, v := range m {
pairs = append(pairs, kv{k, v})
}
slices.SortFunc(pairs, func(a, b kv) int { return cmp.Compare(a.V, b.V) })
maps.Clone 与 maps.Equal 则常用于「快照对比」:
clone := maps.Clone(m)
fmt.Println("equal:", maps.Equal(m, clone)) // true
9.2.8 实战:把 map 索引摊成有序列表
MemStore 用 map[int64]task.Task 存任务,遍历顺序随机。要给它加一个「按标题排序的列表」,标准写法是「取键排序 → 按序取值 → 再按标题排序」:
index := map[int64]task.Task{
3: task.New(3, "补充测试"),
1: task.New(1, "写第 9 章"),
2: task.New(2, "复习 slices"),
}
ids := slices.Sorted(maps.Keys(index))
out := make([]task.Task, 0, len(index))
for _, id := range ids {
out = append(out, index[id])
}
slices.SortFunc(out, func(a, b task.Task) int {
return cmp.Compare(a.Title, b.Title)
})
for _, t := range out {
fmt.Printf("%d %s\n", t.ID, t.Title)
}
实测输出:
1 写第 9 章
2 复习 slices
3 补充测试
第一段 slices.Sorted(maps.Keys(index)) 先按 ID 建立稳定顺序,第二段再按标题排序。如果只关心标题顺序,其实可以跳过第一段,直接遍历 map 收集——但显式排序 ID 的好处是结果与 map 的随机遍历无关,可复现、可测试。
这段代码还演示了泛型的组合威力:maps.Keys 产出的 iter.Seq[int64] 直接被 slices.Sorted 消费,两个泛型 API 像管道一样接在一起,中间没有一次类型断言。
9.2.9 稳定性、性能与陷阱
把本节所有易错点汇总:
| 陷阱 | 说明 | 对策 |
|---|---|---|
SortFunc 不稳定 | 相等元素相对顺序可能变 | 需稳定排序用 SortStableFunc |
DeleteFunc 原地改 | 破坏原切片 | 先 slices.Clone |
Compact 只去相邻 | 非相邻重复保留 | 先排序或用 map |
Keys 返回迭代器 | 不能直接索引 | 用 slices.Sorted 或 slices.Collect |
Contains 需 comparable | 结构体含切片则不行 | 用 ContainsFunc |
| 排序比较函数返回 bool | 签名不匹配 | 用 cmp.Compare 返回 int |
关于性能:slices.Sort 用的是 pdqsort,平均 O(n log n),比手写冒泡快得多;Compact、DeleteFunc 都是 O(n) 单趟扫描。除非有实测数据,否则不要为了「性能」手写循环——标准库的泛型实现经过大量基准测试,你的手写版大概率更慢。
9.2.10 小结
slices/maps/cmp 是泛型最成功的应用:它们把「对集合的常见操作」变成了可组合的积木,而且类型安全、无需断言。记住三条使用纪律:
- 先查标准库,再写循环。绝大多数集合操作已经有人写好并优化过。
- 注意「原地修改」。
Delete/Compact/Insert都会改底层数组,需要保留原数据时先Clone。 - 排序用
cmp.Compare拼比较函数,别手写if a < b的三段式。
下一节我们换个角度:泛型这么好用,是不是到处都该用?答案是否定的。下一节讨论泛型的取舍——什么时候用泛型、什么时候用接口、什么时候老老实实写具体类型。
阅读导航:上一节:9.1 类型参数与约束 · 下一节:9.3 泛型的取舍 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。