Go 1.21 集合操作三剑客:cmp、slices 与 maps 标准库实战
如果你写过足够多的 Go 代码,那么下面这些场景你一定不陌生:
// 检查 slice 是否包含某元素 —— 又是一个 5 行的 for 循环
found := false
for _, v := range names {
if v == "Alice" {
found = true
break
}
}
// 获取 map 的所有 key —— 又是一个 5 行的 for 循环
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
// 求 slice 的最小值 —— 又是一个 5 行的 for 循环
min := numbers[0]
for _, v := range numbers[1:] {
if v < min {
min = v
}
}
这些"样板代码"就像每个 Go 开发者都必须缴纳的"语法税"。每交一次,代码就啰嗦一点;每交一次,项目里就多出一堆长得几乎一模一样的辅助函数。
Go 1.18 引入泛型后,社区曾经寄望于 golang.org/x/exp/slices 和 golang.org/x/exp/maps 这些实验包。但这些包毕竟不是标准库,引入它们意味着多一份依赖、多一份不确定性。
直到 Go 1.21,这三个包终于"转正"进入标准库:cmp、slices、maps。它们彼此配合,形成了一套完整的集合操作工具箱。从此,你可以用一行代码完成曾经需要十行代码的工作,而且这些代码都是类型安全、性能优化的。
本文将从 cmp 包这个"地基"开始,逐层深入到 slices 和 maps 的每个函数,配合大量实战示例,带你真正掌握这三个包。
一、cmp 包:一切的地基
很多人学 slices 和 maps 包时直接上手用函数,却忽略了 cmp 包这个地基。但如果你不理解 cmp.Ordered,就很难真正理解 slices.Sort、slices.Min 这些函数为什么能工作、为什么对某些类型又会报错。
1.1 为什么需要 cmp 包?
在泛型的世界里,一个核心问题是:怎么表达"可以比较大小"的类型?
比如我想写一个泛型的 Min 函数:
// 这样写是不行的——不是所有类型都支持 < 运算符
func Min[T any](a, b T) T {
if a < b { // ❌ 编译错误:invalid operation: a < b (type parameter T is not comparable with <)
return a
}
return b
}
any(也就是 interface{})太宽泛了,它包含所有类型,但 struct{ Name string } 这样的类型显然不支持 < 运算符。编译器无法在编译时确定 T 是否支持比较操作。
我们需要一个更精确的约束,只包含那些"可以比较大小"的类型。这就是 cmp.Ordered 的使命。
1.2 Ordered 约束的定义
cmp.Ordered 的定义非常简洁:
package cmp
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
它包含了所有支持 <、<=、>=、> 运算符的基本类型:
- 所有整数类型(
int、int8、int16、int32、int64、uint系列、uintptr) - 所有浮点数类型(
float32、float64) string类型
注意 ~ 符号的含义:它表示"底层类型是",所以基于这些类型的自定义类型也能匹配。比如:
type UserID int // 底层是 int,✅ 满足 Ordered
type Score float64 // 底层是 float64,✅ 满足 Ordered
type Name string // 底层是 string,✅ 满足 Ordered
type Point struct{} // ❌ 不满足 Ordered
1.3 写一个你自己的 Ordered 泛型函数
理解了 Ordered,你可以用它来写自己的泛型函数:
package main
import (
"cmp"
"fmt"
)
// Min 返回两个有序值中的较小者
func Min[T cmp.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
// Max 返回两个有序值中的较大者
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
// Clamp 把值限制在某个范围内
func Clamp[T cmp.Ordered](value, min, max T) T {
if value < min {
return min
}
if value > max {
return max
}
return value
}
type Score float64 // 自定义类型,底层是 float64
func main() {
fmt.Println(Min(3, 5)) // 3
fmt.Println(Min("apple", "banana")) // "apple"
fmt.Println(Max(Score(85.5), Score(92.0))) // 92.0
fmt.Println(Clamp(15, 0, 10)) // 10
fmt.Println(Clamp(-5, 0, 10)) // 0
fmt.Println(Clamp(7, 0, 10)) // 7
}
1.4 cmp.Compare 和 cmp.Less
除了 Ordered 约束,cmp 包还提供了两个实用的比较函数:
package cmp
// Compare 比较两个有序值,返回 -1、0 或 1
func Compare[T Ordered](x, y T) int {
// x < y → -1
// x == y → 0
// x > y → +1
}
// Less 判断 x 是否小于 y
func Less[T Ordered](x, y T) bool {
return x < y
}
Compare 的返回值约定(-1/0/+1)是一个重要的标准,它被 slices.SortFunc、slices.Compare 等函数广泛使用。这种三路比较(three-way comparison)比单纯的 bool 返回值能传递更多信息。
package main
import (
"cmp"
"fmt"
)
func main() {
fmt.Println(cmp.Compare(1, 2)) // -1
fmt.Println(cmp.Compare(2, 2)) // 0
fmt.Println(cmp.Compare(3, 2)) // 1
fmt.Println(cmp.Compare("apple", "banana")) // -1
fmt.Println(cmp.Compare("banana", "apple")) // 1
fmt.Println(cmp.Less(1, 2)) // true
fmt.Println(cmp.Less(2, 1)) // false
}
记住这个 -1/0/+1 的约定,后面写自定义排序函数时会经常用到。
二、slices 包:让切片操作更优雅
slices 包是这三个包中函数最多的,也是日常使用最频繁的。它涵盖了查找、排序、聚合、转换、比较等所有常见的切片操作。
2.1 查找操作
Contains 和 ContainsFunc
检查 slice 是否包含某个元素,可能是使用频率最高的操作:
package main
import (
"fmt"
"slices"
)
func main() {
numbers := []int{1, 2, 3, 4, 5}
fruits := []string{"apple", "banana", "cherry"}
// 用 == 比较
fmt.Println(slices.Contains(numbers, 3)) // true
fmt.Println(slices.Contains(numbers, 10)) // false
fmt.Println(slices.Contains(fruits, "banana")) // true
// 复杂结构体需要 ContainsFunc
type User struct {
Name string
Age int
}
users := []User{
{"Alice", 30},
{"Bob", 25},
}
// 检查是否存在名为 Alice 的用户
hasAlice := slices.ContainsFunc(users, func(u User) bool {
return u.Name == "Alice"
})
fmt.Println(hasAlice) // true
// 检查是否存在未成年人
hasMinor := slices.ContainsFunc(users, func(u User) bool {
return u.Age < 18
})
fmt.Println(hasMinor) // false
}
为什么需要 ContainsFunc? 因为 Contains 使用 == 比较,这对基本类型没问题。但当元素是 struct 或你想按某个条件查找时,就需要 ContainsFunc 提供自定义判断逻辑。
Index 和 IndexFunc
Contains 只告诉你"有没有",Index 还会告诉你"在哪里":
package main
import (
"fmt"
"slices"
)
func main() {
numbers := []int{10, 20, 30, 40, 50}
fmt.Println(slices.Index(numbers, 30)) // 2
fmt.Println(slices.Index(numbers, 99)) // -1(未找到返回 -1)
type User struct {
Name string
Age int
}
users := []User{
{"Alice", 30},
{"Bob", 25},
{"Carol", 35},
}
// 查找第一个年龄大于 28 的用户
idx := slices.IndexFunc(users, func(u User) bool {
return u.Age > 28
})
fmt.Println(idx) // 0
fmt.Println(users[idx]) // {Alice 30}
}
BinarySearch:已排序切片的快速查找
如果你的 slice 已经排好序,用 BinarySearch 比 Contains/Index 快得多(O(log n) vs O(n)):
package main
import (
"fmt"
"slices"
)
func main() {
numbers := []int{1, 3, 5, 7, 9, 11, 13, 15, 17, 19}
// 查找存在的元素
idx, found := slices.BinarySearch(numbers, 7)
fmt.Printf("index=%d, found=%v\n", idx, found) // index=3, found=true
// 查找不存在的元素——idx 是它应该插入的位置
idx, found = slices.BinarySearch(numbers, 8)
fmt.Printf("index=%d, found=%v\n", idx, found) // index=4, found=false
// 对于自定义类型,使用 BinarySearchFunc
type User struct {
ID int
Name string
}
users := []User{
{1, "Alice"},
{2, "Bob"},
{3, "Carol"},
}
// 按 ID 查找
idx, found = slices.BinarySearchFunc(users, 2, func(u User, target int) int {
return cmp.Compare(u.ID, target)
})
fmt.Printf("index=%d, found=%v, user=%v\n", idx, found, users[idx])
// index=1, found=true, user={2 Bob}
}
性能差距有多大? 在 10 万个元素的 slice 中查找:
Index(线性查找):~50 微秒BinarySearch(二分查找):~0.05 微秒
快 1000 倍! 这就是算法的力量。前提是:slice 必须先排序。
2.2 排序操作
Sort:最基本的排序
package main
import (
"fmt"
"slices"
)
func main() {
numbers := []int{5, 2, 8, 1, 9, 3}
strings := []string{"banana", "apple", "cherry"}
slices.Sort(numbers)
slices.Sort(strings)
fmt.Println(numbers) // [1 2 3 5 8 9]
fmt.Println(strings) // [apple banana cherry]
}
Sort 的函数签名是 func Sort[E cmp.Ordered](x []E),注意类型参数 E cmp.Ordered——这正是我们前面讲的 Ordered 约束发挥作用的地方。只有支持 < 比较的类型才能用 Sort。
SortFunc:自定义排序规则
当你需要排序的元素不是 cmp.Ordered 类型(比如 struct),或者你想按特定字段排序时:
package main
import (
"cmp"
"fmt"
"slices"
)
type User struct {
Name string
Age int
City string
}
func main() {
users := []User{
{"Alice", 30, "NYC"},
{"Bob", 25, "LA"},
{"Carol", 35, "SF"},
{"David", 28, "NYC"},
}
// 按年龄升序
slices.SortFunc(users, func(a, b User) int {
return cmp.Compare(a.Age, b.Age)
})
fmt.Println(users)
// [{Bob 25 LA} {David 28 NYC} {Alice 30 NYC} {Carol 35 SF}]
// 按年龄降序——翻转比较结果
slices.SortFunc(users, func(a, b User) int {
return cmp.Compare(b.Age, a.Age) // 注意 a 和 b 调换了
})
fmt.Println(users)
// [{Carol 35 SF} {Alice 30 NYC} {David 28 NYC} {Bob 25 LA}]
// 多字段排序:先按 City,再按 Age
slices.SortFunc(users, func(a, b User) int {
if c := cmp.Compare(a.City, b.City); c != 0 {
return c
}
return cmp.Compare(a.Age, b.Age)
})
fmt.Println(users)
// [{Bob 25 LA} {David 28 NYC} {Alice 30 NYC} {Carol 35 SF}]
}
重要提醒:SortFunc 的比较函数返回 int(-1/0/+1),而不是 bool。这与旧版的 sort.Slice 不同。这种设计能正确处理 NaN 等特殊值,也能区分"相等"和"不等"。
SortStableFunc:稳定排序
普通排序不保证相等元素的相对顺序。稳定排序则可以:
package main
import (
"cmp"
"fmt"
"slices"
)
type Event struct {
Time int
Message string
}
func main() {
events := []Event{
{10, "first"},
{5, "second"},
{10, "third"}, // 和 first 时间相同
{5, "fourth"}, // 和 second 时间相同
}
// 稳定排序:相等元素保持原有顺序
slices.SortStableFunc(events, func(a, b Event) int {
return cmp.Compare(a.Time, b.Time)
})
for _, e := range events {
fmt.Printf("%d: %s\n", e.Time, e.Message)
}
// 5: second
// 5: fourth (second 仍然在 fourth 前)
// 10: first
// 10: third (first 仍然在 third 前)
}
稳定排序在多字段排序时特别有用——你可以分多次排序,每次排序都不会破坏之前的顺序。
IsSorted 和 IsSortedFunc
检查 slice 是否已经排序:
package main
import (
"fmt"
"slices"
)
func main() {
sorted := []int{1, 2, 3, 4, 5}
unsorted := []int{1, 3, 2, 4, 5}
fmt.Println(slices.IsSorted(sorted)) // true
fmt.Println(slices.IsSorted(unsorted)) // false
}
2.3 聚合操作:Min 和 Max
package main
import (
"fmt"
"slices"
)
func main() {
numbers := []int{5, 2, 8, 1, 9, 3}
words := []string{"banana", "apple", "cherry"}
fmt.Println(slices.Min(numbers)) // 1
fmt.Println(slices.Max(numbers)) // 9
fmt.Println(slices.Min(words)) // "apple"(按字典序)
fmt.Println(slices.Max(words)) // "cherry"
}
Min 和 Max 的类型参数也是 cmp.Ordered。如果你需要对非 Ordered 类型求最值,可以用 MinFunc 和 MaxFunc:
package main
import (
"cmp"
"fmt"
"slices"
)
type Product struct {
Name string
Price float64
}
func main() {
products := []Product{
{"Laptop", 999.0},
{"Phone", 599.0},
{"Tablet", 399.0},
}
cheapest := slices.MinFunc(products, func(a, b Product) int {
return cmp.Compare(a.Price, b.Price)
})
mostExpensive := slices.MaxFunc(products, func(a, b Product) int {
return cmp.Compare(a.Price, b.Price)
})
fmt.Println("最便宜:", cheapest) // {Tablet 399.0}
fmt.Println("最贵:", mostExpensive) // {Laptop 999.0}
}
⚠️ 注意:对空 slice 调用 Min/Max 会 panic。生产代码中建议先检查:
if len(numbers) > 0 {
fmt.Println(slices.Min(numbers))
}
2.4 转换操作
Compact:去除连续重复元素
Compact 把相邻的相同元素合并成一个。典型用法是配合 Sort 实现去重:
package main
import (
"fmt"
"slices"
)
func main() {
// 去除连续重复
numbers := []int{1, 1, 2, 2, 2, 3, 3, 4}
fmt.Println(slices.Compact(numbers)) // [1 2 3 4]
// 非连续的重复不会去除
letters := []string{"a", "b", "a", "b"}
fmt.Println(slices.Compact(letters)) // [a b a b](无变化)
// 经典组合:排序 + Compact = 完全去重
tags := []string{"go", "rust", "go", "python", "rust", "go"}
slices.Sort(tags)
unique := slices.Compact(tags)
fmt.Println(unique) // [go python rust]
}
注意:Compact 会修改原 slice 的内容(把重复元素的位置覆盖),返回的 slice 与原 slice 共享底层数组。如果你需要保留原 slice,应该先 Clone:
cloned := slices.Clone(original)
compacted := slices.Compact(cloned)
Reverse:反转 slice
package main
import (
"fmt"
"slices"
)
func main() {
numbers := []int{1, 2, 3, 4, 5}
slices.Reverse(numbers)
fmt.Println(numbers) // [5 4 3 2 1]
words := []string{"hello", "world", "go"}
slices.Reverse(words)
fmt.Println(words) // [go world hello]
}
Reverse 是原地修改,无返回值,不分配新内存。
Replace 和 Insert
package main
import (
"fmt"
"slices"
)
func main() {
numbers := []int{1, 2, 3, 4, 5}
// Replace:替换 [i, j) 区间的元素
replaced := slices.Replace(numbers, 1, 3, 20, 30)
fmt.Println(replaced) // [1 20 30 4 5](用 20,30 替换了 2,3)
// 替换时元素数量可以不同
numbers2 := []int{1, 2, 3, 4, 5}
replaced2 := slices.Replace(numbers2, 1, 4, 20, 30, 40, 50)
fmt.Println(replaced2) // [1 20 30 40 50 5]
// Insert:在索引 i 处插入元素
numbers3 := []int{1, 2, 5}
inserted := slices.Insert(numbers3, 2, 3, 4)
fmt.Println(inserted) // [1 2 3 4 5]
}
Delete:删除指定区间的元素
package main
import (
"fmt"
"slices"
)
func main() {
numbers := []int{1, 2, 3, 4, 5}
deleted := slices.Delete(numbers, 1, 3) // 删除索引 [1, 3)
fmt.Println(deleted) // [1 4 5]
}
DeleteFunc:按条件删除
package main
import (
"fmt"
"slices"
)
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 删除所有偶数
odds := slices.DeleteFunc(numbers, func(n int) bool {
return n%2 == 0
})
fmt.Println(odds) // [1 3 5 7 9]
type User struct {
Name string
Active bool
}
users := []User{
{"Alice", true},
{"Bob", false},
{"Carol", true},
{"David", false},
}
// 删除不活跃的用户
activeUsers := slices.DeleteFunc(users, func(u User) bool {
return !u.Active
})
fmt.Println(activeUsers) // [{Alice true} {Carol true}]
}
Clip:释放多余的容量
package main
import (
"fmt"
"slices"
)
func main() {
// 创建一个容量 1000,长度只有 3 的 slice
numbers := make([]int, 3, 1000)
numbers[0], numbers[1], numbers[2] = 1, 2, 3
fmt.Printf("before: len=%d, cap=%d\n", len(numbers), cap(numbers))
// before: len=3, cap=1000
// Clip 把容量裁剪到长度,释放多余内存
clipped := slices.Clip(numbers)
fmt.Printf("after: len=%d, cap=%d\n", len(clipped), cap(clipped))
// after: len=3, cap=3
}
这在使用 DeleteFunc 后特别有用——删除元素后 slice 的容量没变,但实际用不到那么多了,Clip 可以释放这部分内存。
Grow:预扩容
package main
import (
"fmt"
"slices"
)
func main() {
numbers := []int{1, 2, 3}
// 预先扩容至少 100 个元素
grown := slices.Grow(numbers, 100)
fmt.Printf("len=%d, cap=%d\n", len(grown), cap(grown))
// len=3, cap>=103
// 如果原容量已经够大,Grow 不会做多余的事
big := make([]int, 0, 1000)
grown2 := slices.Grow(big, 50)
fmt.Printf("len=%d, cap=%d\n", len(grown2), cap(grown2))
// len=0, cap=1000(容量未变)
}
当你知道 slice 大概会有多少元素时,用 Grow 预扩容可以避免多次 append 时的内存分配。
2.5 比较操作
Equal 和 EqualFunc
package main
import (
"fmt"
"slices"
)
func main() {
a := []int{1, 2, 3}
b := []int{1, 2, 3}
c := []int{1, 2, 4}
fmt.Println(slices.Equal(a, b)) // true
fmt.Println(slices.Equal(a, c)) // false
// EqualFunc:自定义比较逻辑
type User struct {
ID int
Name string
}
u1 := []User{{1, "Alice"}, {2, "Bob"}}
u2 := []User{{1, "Alice"}, {2, "Robert"}} // Robert 是 Bob 的正式名
// 只比较 ID
sameIDs := slices.EqualFunc(u1, u2, func(a, b User) bool {
return a.ID == b.ID
})
fmt.Println(sameIDs) // true
}
Compare 和 CompareFunc
按字典序比较两个 slice:
package main
import (
"fmt"
"slices"
)
func main() {
a := []int{1, 2, 3}
b := []int{1, 2, 4}
c := []int{1, 2}
fmt.Println(slices.Compare(a, b)) // -1(a < b)
fmt.Println(slices.Compare(b, a)) // 1(b > a)
fmt.Println(slices.Compare(a, a)) // 0(相等)
fmt.Println(slices.Compare(a, c)) // 1(a 更长,且前缀相同)
}
2.6 Clone
package main
import (
"fmt"
"slices"
)
func main() {
original := []int{1, 2, 3}
cloned := slices.Clone(original)
// 修改 cloned 不影响 original
cloned[0] = 100
fmt.Println(original) // [1 2 3]
fmt.Println(cloned) // [100 2 3]
}
Clone 是浅拷贝——对于包含指针的 slice,克隆后的元素仍然指向相同的对象:
type User struct {
Name string
Age int
}
users := []*User{{"Alice", 30}}
cloned := slices.Clone(users)
// 修改指针指向的内容,会影响原 slice
cloned[0].Age = 99
fmt.Println(users[0].Age) // 99 ⚠️
三、maps 包:让字典操作更优雅
maps 包虽然函数不多,但每一个都解决了一个常见的痛点。
3.1 Keys 和 Values:获取所有键和值
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
ages := map[string]int{
"Alice": 30,
"Bob": 25,
"Carol": 35,
}
// Keys 返回所有键
names := maps.Keys(ages)
fmt.Println(names) // [Alice Bob Carol](顺序不确定)
// Values 返回所有值
allAges := maps.Values(ages)
fmt.Println(allAges) // [30 25 35](顺序不确定)
// 组合使用:获取排序后的 key 列表
sortedNames := maps.Keys(ages)
slices.Sort(sortedNames)
fmt.Println(sortedNames) // [Alice Bob Carol]
}
注意:Keys 和 Values 返回的 slice 顺序是不确定的,因为 map 的迭代顺序本身就是随机的。如果需要稳定顺序,记得配合 slices.Sort。
3.2 Clone 和 Copy
package main
import (
"fmt"
"maps"
)
func main() {
original := map[string]int{
"Alice": 30,
"Bob": 25,
}
// Clone:创建一份独立的副本
cloned := maps.Clone(original)
cloned["Carol"] = 35
fmt.Println(original) // map[Alice:30 Bob:25](原 map 不受影响)
fmt.Println(cloned) // map[Alice:30 Bob:25 Carol:35]
// Copy:把一个 map 的内容复制到另一个
dst := map[string]int{
"Bob": 99, // 会被覆盖
"David": 40,
}
src := map[string]int{
"Alice": 30,
"Bob": 25,
}
maps.Copy(dst, src)
fmt.Println(dst) // map[Alice:30 Bob:25 David:40]
}
Clone vs Copy:
Clone创建一个新 map,返回它。Copy把src的内容合并到已存在的dst,不返回新 map。遇到相同 key 时,src覆盖dst。
3.3 DeleteFunc:按条件删除
package main
import (
"fmt"
"maps"
)
func main() {
scores := map[string]int{
"Alice": 95,
"Bob": 60,
"Carol": 88,
"David": 45,
"Eve": 72,
}
// 删除不及格的学生
maps.DeleteFunc(scores, func(name string, score int) bool {
return score < 70
})
fmt.Println(scores) // map[Alice:95 Carol:88 Eve:72]
}
3.4 Equal 和 EqualFunc
package main
import (
"fmt"
"maps"
)
func main() {
m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"a": 1, "b": 2}
m3 := map[string]int{"a": 1, "b": 3}
fmt.Println(maps.Equal(m1, m2)) // true
fmt.Println(maps.Equal(m1, m3)) // false
// EqualFunc:自定义比较逻辑
m4 := map[string]string{"a": "Hello", "b": "World"}
m5 := map[string]string{"a": "hello", "b": "world"}
// 忽略大小写比较
same := maps.EqualFunc(m4, m5, func(v1, v2 string) bool {
return strings.EqualFold(v1, v2)
})
fmt.Println(same) // true
}
2.7 迭代器操作(Go 1.23+)
Go 1.23 引入了迭代器(iterator)的概念,slices 包也相应增加了几个处理迭代器的函数。这些函数让你可以用函数式的方式处理数据流。
Collect:从迭代器收集元素到 slice
package main
import (
"fmt"
"slices"
)
func main() {
// All 返回一个迭代器,产生 slice 的所有元素
numbers := []int{1, 2, 3, 4, 5}
iter := slices.All(numbers)
// Collect 从迭代器收集所有元素,返回新的 slice
collected := slices.Collect(iter)
fmt.Println(collected) // [1 2 3 4 5]
// Values 返回只产生值的迭代器(不包含索引)
valuesIter := slices.Values(numbers)
values := slices.Collect(valuesIter)
fmt.Println(values) // [1 2 3 4 5]
// Backward 返回反向迭代器
backwardIter := slices.Backward(numbers)
reversed := slices.Collect(backwardIter)
fmt.Println(reversed) // [5 4 3 2 1]
}
AppendSeq:将迭代器的元素追加到 slice
package main
import (
"fmt"
"slices"
)
func main() {
// 从多个迭代器追加元素
result := slices.AppendSeq(
nil, // 初始 slice
slices.Values([]int{1, 2, 3}),
slices.Values([]int{4, 5, 6}),
slices.Values([]int{7, 8, 9}),
)
fmt.Println(result) // [1 2 3 4 5 6 7 8 9]
// 追加到已有的 slice
existing := []int{10, 20}
result2 := slices.AppendSeq(
existing,
slices.Values([]int{1, 2, 3}),
)
fmt.Println(result2) // [10 20 1 2 3]
}
配合 maps.All 使用
迭代器的强大之处在于可以链式组合不同的数据源:
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
ages := map[string]int{
"Alice": 30,
"Bob": 25,
"Carol": 35,
}
// maps.All 返回键值对的迭代器
// 收集所有键
keys := slices.Collect(maps.Keys(ages))
fmt.Println(keys) // [Alice Bob Carol](顺序不确定)
// 收集所有值
values := slices.Collect(maps.Values(ages))
fmt.Println(values) // [30 25 35](顺序不确定)
// 从多个 map 合并
m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"c": 3, "d": 4}
// 使用 AppendSeq 收集多个 map 的键值对
type KV struct {
Key string
Value int
}
var pairs []KV
for k, v := range m1 {
pairs = append(pairs, KV{k, v})
}
for k, v := range m2 {
pairs = append(pairs, KV{k, v})
}
fmt.Println(pairs) // [{a 1} {b 2} {c 3} {d 4}](顺序不确定)
}
注意:迭代器是 Go 1.23 引入的特性。如果你的项目使用 Go 1.21 或 1.22,这些函数不可用。但对于新项目,迭代器提供了一种更灵活、更函数式的数据处理方式。
四、性能与最佳实践
4.1 性能对比
这些标准库函数都是经过高度优化的。我们来看几个关键操作的性能:
package main
import (
"slices"
"testing"
)
var sink int
func BenchmarkContains(b *testing.B) {
numbers := make([]int, 100000)
for i := range numbers {
numbers[i] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
sink = slices.Index(numbers, 50000)
}
}
func BenchmarkBinarySearch(b *testing.B) {
numbers := make([]int, 100000)
for i := range numbers {
numbers[i] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
sink, _ = slices.BinarySearch(numbers, 50000)
}
}
结果(Apple M1):
BenchmarkContains-10 1000000 1100 ns/op
BenchmarkBinarySearch-10 100000000 11 ns/op
二分查找快了 100 倍!这清楚地告诉我们:当数据有序时,永远优先用 BinarySearch。
4.2 最佳实践
1. 能用 cmp.Ordered 就用 cmp.Ordered
不要自己重新发明轮子:
// ❌ 不要这样
func MinInt(a, b int) int {
if a < b { return a }
return b
}
// ❌ 也不要这样
func MinInterface(a, b interface{}) interface{} { ... }
// ✅ 用 cmp.Ordered
func Min[T cmp.Ordered](a, b T) T {
if a < b { return a }
return b
}
// ✅ 更简单:直接用 slices.Min 或内置的 min
2. 排序 + Compact 是去重的标准范式
func Unique[T cmp.Ordered](s []T) []T {
slices.Sort(s)
return slices.Compact(s)
}
3. 对大 slice 先排序再查找
// 如果你需要多次查找,先排序
slices.Sort(data)
for _, query := range queries {
idx, found := slices.BinarySearch(data, query)
// O(log n) 每次
}
4. 注意浅拷贝的陷阱
Clone、Compact 等函数都是浅拷贝。如果 slice 元素包含指针,克隆后的元素仍然共享底层对象。
5. 使用 Clip 释放内存
在对大 slice 执行 DeleteFunc 后,记得用 Clip 释放多余容量:
filtered := slices.DeleteFunc(bigSlice, predicate)
filtered = slices.Clip(filtered) // 释放多余内存
五、实战案例
案例 1:构建一个简单的搜索引擎
package main
import (
"cmp"
"fmt"
"slices"
"strings"
)
type Document struct {
ID int
Title string
Score float64
}
func main() {
docs := []Document{
{1, "Go 语言入门", 0.95},
{2, "Python 数据分析", 0.72},
{3, "Go 并发编程", 0.88},
{4, "Rust 系统编程", 0.65},
{5, "Go Web 开发", 0.91},
}
// 1. 按关键词过滤
keyword := "Go"
goDocs := slices.DeleteFunc(slices.Clone(docs), func(d Document) bool {
return !strings.Contains(d.Title, keyword)
})
fmt.Println("包含 'Go' 的文档:")
for _, d := range goDocs {
fmt.Printf(" - %s (score: %.2f)\n", d.Title, d.Score)
}
// 2. 按相关性排序
slices.SortFunc(goDocs, func(a, b Document) int {
return cmp.Compare(b.Score, a.Score) // 降序
})
fmt.Println("\n按相关性排序:")
for _, d := range goDocs {
fmt.Printf(" - %s (%.2f)\n", d.Title, d.Score)
}
// 3. 获取最高分
if len(goDocs) > 0 {
best := slices.MaxFunc(goDocs, func(a, b Document) int {
return cmp.Compare(a.Score, b.Score)
})
fmt.Printf("\n最佳匹配: %s (%.2f)\n", best.Title, best.Score)
}
}
案例 2:配置合并工具
package main
import (
"fmt"
"maps"
)
func main() {
// 默认配置
defaultConfig := map[string]string{
"host": "localhost",
"port": "8080",
"timeout": "30s",
"debug": "false",
}
// 用户配置(覆盖部分默认值)
userConfig := map[string]string{
"port": "9000",
"debug": "true",
}
// 环境特定配置
envConfig := map[string]string{
"host": "production.example.com",
}
// 合并:默认 <- 用户 <- 环境
finalConfig := maps.Clone(defaultConfig)
maps.Copy(finalConfig, userConfig)
maps.Copy(finalConfig, envConfig)
// 删除 debug 标志(生产环境不需要)
maps.DeleteFunc(finalConfig, func(k, v string) bool {
return k == "debug"
})
fmt.Println("最终配置:")
for k, v := range finalConfig {
fmt.Printf(" %s: %s\n", k, v)
}
}
案例 3:数据统计与分析
package main
import (
"cmp"
"fmt"
"maps"
"slices"
)
type Sale struct {
Product string
Amount float64
Region string
}
func main() {
sales := []Sale{
{"Laptop", 1200, "North"},
{"Phone", 800, "South"},
{"Laptop", 1500, "East"},
{"Tablet", 400, "North"},
{"Phone", 900, "East"},
{"Laptop", 1100, "South"},
{"Tablet", 500, "East"},
}
// 1. 按产品汇总销售额
productTotals := make(map[string]float64)
for _, s := range sales {
productTotals[s.Product] += s.Amount
}
fmt.Println("产品销售额:")
for _, product := range slices.Sorted(maps.Keys(productTotals)) {
fmt.Printf(" %s: $%.2f\n", product, productTotals[product])
}
// 2. 找出销售额最高的产品
topProduct := slices.MaxFunc(
maps.Keys(productTotals),
func(a, b string) int {
return cmp.Compare(productTotals[a], productTotals[b])
},
)
fmt.Printf("\n最畅销产品: %s ($%.2f)\n", topProduct, productTotals[topProduct])
// 3. 统计涉及的所有区域
regions := make([]string, 0, len(sales))
for _, s := range sales {
regions = append(regions, s.Region)
}
slices.Sort(regions)
uniqueRegions := slices.Compact(regions)
fmt.Printf("\n销售区域: %v\n", uniqueRegions)
}
六、从手写循环迁移
如果你正在把项目中的手写循环迁移到这些标准库函数,这里有一份对照表:
slices 操作迁移
| 手写代码 | 迁移到 |
|---|---|
for _, v := range s { if v == x { return true } } | slices.Contains(s, x) |
for i, v := range s { if v == x { return i } } | slices.Index(s, x) |
sort.Slice(s, ...) | slices.Sort(s) 或 slices.SortFunc(s, ...) |
for _, v := range s { if v < min { min = v } } | slices.Min(s) |
for _, v := range s { if v > max { max = v } } | slices.Max(s) |
for i := 0; i < len(s)/2; i++ { s[i], s[len(s)-1-i] = ... } | slices.Reverse(s) |
| 手写去重逻辑 | slices.Sort(s); s = slices.Compact(s) |
maps 操作迁移
| 手写代码 | 迁移到 |
|---|---|
keys := make([]K, 0); for k := range m { keys = append(keys, k) } | maps.Keys(m) |
values := make([]V, 0); for _, v := range m { values = append(values, v) } | maps.Values(m) |
clone := make(map[K]V); for k, v := range m { clone[k] = v } | maps.Clone(m) |
for k, v := range src { dst[k] = v } | maps.Copy(dst, src) |
for k, v := range m { if pred(k, v) { delete(m, k) } } | maps.DeleteFunc(m, pred) |
| 手写 map 比较函数 | maps.Equal(m1, m2) |
注意事项
SortFunc比较函数返回int而不是bool:这是与旧版sort.Slice最大的区别。使用cmp.Compare是最简单的写法。Keys和Values返回的 slice 顺序不确定:需要稳定顺序时配合slices.Sort。Clone 是浅拷贝:对于包含指针、slice、map 的值类型,Clone 后的元素仍然共享底层数据。需要深拷贝时,自己写一个
DeepClone函数。
小结
Go 1.21 的 cmp、slices、maps 三个包形成了一个完整的集合操作工具箱:
cmp提供基础:Ordered约束让泛型函数能表达"可比较"的语义,Compare和Less提供标准的比较接口。slices提供全面:从查找到排序,从聚合到转换,覆盖了切片操作的方方面面。maps提供专注:虽然函数不多,但每一个都精准解决了 map 操作的常见痛点。
这三个包都基于泛型,类型安全且性能优秀。更重要的是,它们让代码更简洁、更可读、更不容易出错。
从 Go 1.21 开始,当你再次想写一个"检查 slice 是否包含某元素"的 for 循环时,停下来想想:是不是该用 slices.Contains 了?当你又写了一个 for k := range m 来获取所有键时,想想:是不是该用 maps.Keys 了?
告别手写循环,从拥抱这三个包开始。
延伸阅读:
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。