Go 用 slices/maps 标准库泛型函数:告别手写 Contains、Sort、去重(Go 1.21)
你有没有在每个 Go 项目里都复制粘贴过一份 sliceContains、sortByField、uniqueStrings 的工具函数?Go 泛型落地前,这些代码只能靠手写,一个项目一份,还容易写错。从 Go 1.21 开始,标准库正式收纳了 slices 和 maps 两个泛型包,把这些高频操作一次性补齐。这篇就把最常用的函数过一遍,顺便讲清楚几个容易踩的坑。
先看看以前有多啰嗦
想判断一个切片里有没有某个元素,Go 1.21 之前得自己写循环:
go
// 朴素写法:每个项目都要来一份
func contains(s []string, target string) bool {
for _, v := range s {
if v == target {
return true
}
}
return false
}
排序稍微复杂一点就得实现 sort.Interface,或者用 sort.Slice 传一个闭包,写法冗长且不类型安全。这些样板代码没有技术含量,却又不得不写。
slices 包:查找、排序、增删一步到位
引入 slices 后,上面那堆全都不用自己写了:
go
package main
import (
"fmt"
"slices"
)
func main() {
nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
// 排序:原地升序,不用再写 sort.Slice 闭包
slices.Sort(nums)
fmt.Println(nums) // [1 1 2 3 4 5 6 9]
// 查找:是否包含
fmt.Println(slices.Contains(nums, 5)) // true
// 二分查找(切片必须已排序),返回下标和是否命中
idx, found := slices.BinarySearch(nums, 4)
fmt.Println(idx, found) // 4 true
// 找第一个满足条件的下标,没有返回 -1
i := slices.IndexFunc(nums, func(n int) bool { return n > 4 })
fmt.Println(i) // 6(值为 5 的位置)
}
自定义排序用 slices.SortFunc,回调返回一个整数(负/零/正表示小于/等于/大于),语义和 C 的 qsort 一致:
go
type User struct {
Name string
Age int
}
users := []User{{"Bob", 30}, {"Alice", 25}, {"Carol", 30}}
// 先按年龄升序,年龄相同按名字字典序
slices.SortFunc(users, func(a, b User) int {
if a.Age != b.Age {
return a.Age - b.Age // 注意:字段是大整数时可能溢出,见下文
}
return strings.Compare(a.Name, b.Name)
})
用 slices 优雅去重
去重是另一个高频需求。slices.Compact 能删掉相邻的重复元素,配合先排序就是标准去重套路:
go
func unique[T cmp.Ordered](s []T) []T {
slices.Sort(s) // 让重复元素相邻
return slices.Compact(s) // 删除相邻重复,返回新长度的切片
}
fmt.Println(unique([]int{3, 1, 1, 4, 3, 3})) // [1 3 4]
这里 cmp.Ordered 是 cmp 包提供的约束,涵盖所有可比较大小的基础类型(整数、浮点、字符串)。如果不想改变原顺序,就别用 Compact,改用 map 记录见过的元素。
maps 包:提取 key、拷贝与比较
maps 包补齐了操作 map 的常用动作。要注意 Go 1.21 里 maps.Keys 返回的是迭代器 (Go 1.23 起是 iter.Seq),需要配合 slices.Collect 收集成切片:
go
import (
"maps"
"slices"
)
m := map[string]int{"a": 1, "b": 2, "c": 3}
// 取出所有 key(顺序随机!),收集成切片再排序
keys := slices.Collect(maps.Keys(m))
slices.Sort(keys)
fmt.Println(keys) // [a b c]
// 浅拷贝一个 map,改副本不影响原 map
cp := maps.Clone(m)
cp["a"] = 99
fmt.Println(m["a"]) // 1,原 map 没变
// 比较两个 map 内容是否相等
fmt.Println(maps.Equal(m, cp)) // false
如果你的 Go 版本是 1.21/1.22,maps.Keys 的签名可能还是老的返回切片版本,升级到 1.23 后统一成迭代器。写代码前 go version 确认一下,避免编译报错找不到北。
三个别踩的坑
第一,slices.Clone 是浅拷贝。 它只复制切片本身的元素,如果元素是指针或含切片的结构体,拷贝后两者仍共享底层数据:
go
type Box struct{ Items []int }
a := []Box{{Items: []int{1, 2}}}
b := slices.Clone(a)
b[0].Items[0] = 99
fmt.Println(a[0].Items[0]) // 99!内层切片是共享的
第二,SortFunc 的比较函数别用减法算大整数。 a.Age - b.Age 在数值接近 int 边界时会溢出,得到错误的正负号。安全写法是用 cmp.Compare(a.Age, b.Age),它内部做了溢出保护。
第三,slices.Compact 只删相邻重复。 没排序的切片直接 Compact 不会全局去重,[1,2,1] 出来还是 [1,2,1]。记住:先 Sort 再 Compact。
小结
- Go 1.21 的
slices/maps泛型包让 Contains、Sort、去重、Clone 这些操作不用再手写工具函数。 - 自定义排序用
slices.SortFunc,比较函数优先用cmp.Compare而不是减法,避免溢出。 - 去重标准套路:
Sort让重复相邻 +Compact删除相邻重复。 Clone是浅拷贝,元素含引用类型时要当心共享;maps.Keys在新版本返回迭代器,用slices.Collect收集。
一句话记忆:能用标准库就别自己撸工具函数------slices 和 maps 已经帮你写好、测好、优化好了。