很多小工具终于不用自己写
Go 早期处理切片和 map 时,经常要自己写辅助函数:判断切片是否包含元素、克隆切片、比较切片、复制 map、按字段排序。每个项目都会有一组差不多的 utils。Go 1.21 把 slices、maps、cmp 等包带进标准库,让这些常见集合操作更统一。
这些包不是让 Go 变成链式集合语言。它们只是把一些稳定、常见、容易写重复的操作标准化。你仍然需要知道什么时候用工具函数,什么时候普通 for 循环更清楚。
这篇文章讲最常用的几个函数。
slices.Contains 和 Index
tags := []string{"Go", "Backend", "Tutorial"}
if slices.Contains(tags, "Go") {
fmt.Println("has Go")
}
index := slices.Index(tags, "Backend")
fmt.Println(index)
以前你可能写:
func ContainsString(items []string, target string) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
}
现在简单场景可以直接用标准库。注意 Contains 是线性查找,小列表很方便。如果要频繁查询大量数据,map 做 set 更合适:
set := make(map[string]struct{}, len(tags))
for _, tag := range tags {
set[tag] = struct{}{}
}
工具函数不能替代数据结构判断。
slices.Clone 和 Equal
切片赋值不是复制:
a := []string{"Go", "PHP"}
b := a
b[0] = "Rust"
fmt.Println(a[0]) // Rust
克隆:
b := slices.Clone(a)
b[0] = "Rust"
fmt.Println(a[0]) // Go
比较:
got := []int{1, 2, 3}
want := []int{1, 2, 3}
if !slices.Equal(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
如果顺序不重要,先排序再比较:
gotSorted := slices.Clone(got)
wantSorted := slices.Clone(want)
slices.Sort(gotSorted)
slices.Sort(wantSorted)
if !slices.Equal(gotSorted, wantSorted) {
t.Fatalf("got %v, want %v", got, want)
}
测试代码会因此少很多自定义辅助函数。
SortFunc 和 cmp.Compare
结构体排序:
type User struct {
Name string
Score int
}
slices.SortFunc(users, func(a, b User) int {
return cmp.Compare(b.Score, a.Score)
})
这里按分数倒序。cmp.Compare(x, y) 会返回 -1、0、1,适合写排序比较函数。
多字段排序:
slices.SortFunc(users, func(a, b User) int {
if n := cmp.Compare(b.Score, a.Score); n != 0 {
return n
}
return cmp.Compare(a.Name, b.Name)
})
先按分数倒序,分数相同按名字升序。这个写法比手写很多 if 更紧凑,但仍然要保持可读。
maps.Clone 和 Equal
复制 map:
source := map[string]int{"go": 1, "php": 2}
copied := maps.Clone(source)
copied["go"] = 10
fmt.Println(source["go"]) // 1
比较 map:
a := map[string]int{"go": 1}
b := map[string]int{"go": 1}
fmt.Println(maps.Equal(a, b))
map 遍历顺序仍然不稳定。如果要稳定输出,提取 key 后排序:
keys := make([]string, 0, len(source))
for key := range source {
keys = append(keys, key)
}
slices.Sort(keys)
for _, key := range keys {
fmt.Println(key, source[key])
}
标准库工具让复制和比较更简单,但不会改变 map 无序这个事实。
Delete、Insert 和 Compact
slices 里还有一些适合日常代码的函数。删除范围:
items := []string{"a", "b", "c", "d"}
items = slices.Delete(items, 1, 3)
fmt.Println(items) // [a d]
插入元素:
items = slices.Insert(items, 1, "x", "y")
fmt.Println(items)
去掉相邻重复值:
nums := []int{1, 1, 2, 2, 2, 3}
nums = slices.Compact(nums)
fmt.Println(nums) // [1 2 3]
注意 Compact 只去掉相邻重复。如果输入是 {1, 2, 1},它不会把最后的 1 去掉。通常需要先排序:
slices.Sort(nums)
nums = slices.Compact(nums)
这些函数都会返回新切片,一定要接住返回值。它们可能复用原底层数组,也可能改变长度。和 append 一样,返回值代表新的切片视图。
什么时候普通循环更好
如果逻辑带有业务含义,普通循环常常更可读:
var visible []Article
for _, article := range articles {
if article.Draft {
continue
}
if article.PublishedAt.After(now) {
continue
}
visible = append(visible, article)
}
这段代码比强行组合多个工具函数更容易读。集合包解决的是常见机械操作,不是替代业务流程。入门阶段可以先在测试和小工具里使用,再逐步判断哪些地方适合放进生产代码。
升级旧工具函数时要小步来
很多老项目里已经有 ContainsString、CloneMap、EqualIntSlice 之类函数。升级到 Go 1.21 后,不一定要一次性全删。更稳的做法是:新代码优先使用标准库;旧工具函数如果没有问题,可以在碰到相关代码时逐步替换。
替换时要注意行为是否完全一致。比如旧函数可能把 nil 切片和空切片当成不同结果,也可能忽略顺序比较。标准库函数有自己的语义,迁移前先补测试:
func TestTagsEqual(t *testing.T) {
got := []string{"Go", "Backend"}
want := []string{"Go", "Backend"}
if !slices.Equal(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
有了测试,再替换实现,风险会小很多。集合工具看起来只是小函数,但它们常被很多业务代码调用,行为差一点也可能影响范围很大。
小结
Go 1.21 的 slices、maps 和 cmp 包让常见集合操作更标准。切片查找用 slices.Contains,克隆用 slices.Clone,比较用 slices.Equal,排序用 slices.SortFunc,map 复制和比较用 maps.Clone、maps.Equal。
这些工具适合减少重复代码,但不要忘记算法和数据结构本身。频繁查询用 map,稳定展示要排序,复杂业务流程用普通循环可能更清楚。标准库工具的价值,是让意图更直接,而不是把所有代码都改成工具函数调用。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。