Go 1.21 slices、maps 和 cmp 入门:集合工具终于标准化

本文讲解 Go 1.21 中 slices、maps 和 cmp 包的常见用法,包括克隆、比较、查找、排序和 map 复制。

很多小工具终于不用自己写

Go 早期处理切片和 map 时,经常要自己写辅助函数:判断切片是否包含元素、克隆切片、比较切片、复制 map、按字段排序。每个项目都会有一组差不多的 utils。Go 1.21 把 slicesmapscmp 等包带进标准库,让这些常见集合操作更统一。

这些包不是让 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)
}

这段代码比强行组合多个工具函数更容易读。集合包解决的是常见机械操作,不是替代业务流程。入门阶段可以先在测试和小工具里使用,再逐步判断哪些地方适合放进生产代码。

升级旧工具函数时要小步来

很多老项目里已经有 ContainsStringCloneMapEqualIntSlice 之类函数。升级到 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 的 slicesmapscmp 包让常见集合操作更标准。切片查找用 slices.Contains,克隆用 slices.Clone,比较用 slices.Equal,排序用 slices.SortFunc,map 复制和比较用 maps.Clonemaps.Equal

这些工具适合减少重复代码,但不要忘记算法和数据结构本身。频繁查询用 map,稳定展示要排序,复杂业务流程用普通循环可能更清楚。标准库工具的价值,是让意图更直接,而不是把所有代码都改成工具函数调用。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页