Go slices 和 maps 包入门:少写一点重复集合代码

本文讲解现代 Go 标准库 slices 和 maps 包的常见用法,包括排序、克隆、比较、删除和 map key 提取。

集合工具终于进入标准库

Go 早期处理切片和 map 时,经常要自己写一些小工具函数:克隆切片、判断相等、删除元素、提取 map 的 key、排序。后来随着泛型进入语言,标准库也逐步提供了 slicesmaps 这类集合辅助包。它们不会把 Go 变成链式集合语言,但能减少很多重复样板代码。

这篇文章讲几个入门最常用的函数。重点不是把 API 列表背完,而是知道什么时候可以用标准库,什么时候普通 for 循环仍然更清楚。

slices.Clone 复制切片

切片赋值不是复制底层数组:

a := []string{"Go", "PHP"}
b := a
b[0] = "Rust"
fmt.Println(a[0]) // Rust

使用 slices.Clone

b := slices.Clone(a)
b[0] = "Rust"
fmt.Println(a[0]) // Go

这比手写更直接:

b := append([]string(nil), a...)

两种都能用。slices.Clone 的好处是意图更明确:我要克隆切片。

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)

以前我们常自己写 ContainsString。现在简单可比较类型可以直接用标准库。

注意 Contains 适合小列表。很大的数据、频繁查询,更适合用 map 做 set:

tagSet := map[string]struct{}{}
for _, tag := range tags {
	tagSet[tag] = struct{}{}
}

工具函数不是算法替代品。数据量和查询频率仍然重要。

slices.Sort 和 SortFunc

基本排序:

nums := []int{3, 1, 2}
slices.Sort(nums)

结构体排序:

type User struct {
	Name  string
	Score int
}

slices.SortFunc(users, func(a, b User) int {
	return b.Score - a.Score
})

SortFunc 的比较函数返回负数、零、正数,表示 a 小于、等于、大于 b。上面用 b.Score - a.Score 实现分数倒序。对于可能溢出的整数,写成显式判断更稳:

slices.SortFunc(users, func(a, b User) int {
	switch {
	case a.Score > b.Score:
		return -1
	case a.Score < b.Score:
		return 1
	default:
		return 0
	}
})

排序仍然是原地修改切片。需要保留原顺序时先 Clone。

maps.Clone 和 keys

复制 map:

source := map[string]int{"go": 1, "php": 2}
copied := maps.Clone(source)
copied["go"] = 10
fmt.Println(source["go"]) // 1

提取 key 的方式会随着 Go 版本和迭代器能力演进有所不同。最稳的入门写法仍然是显式循环:

keys := make([]string, 0, len(source))
for key := range source {
	keys = append(keys, key)
}
slices.Sort(keys)

map 遍历顺序不稳定,如果要展示或测试输出,一定要排序 key。

删除切片元素

假设删除索引 i:

items = slices.Delete(items, i, i+1)

删除范围 [from, to)

items = slices.Delete(items, 2, 5)

Delete 返回新切片,一定要接住返回值。它和 append 一样,会调整切片视图。

如果要按条件过滤,普通循环往往更清楚:

active := items[:0]
for _, item := range items {
	if item.Active {
		active = append(active, item)
	}
}
items = active

这个写法会复用原底层数组,适合性能敏感路径。入门阶段不必强求,但要知道标准库函数和手写循环各有位置。

测试里也能少写辅助函数

集合工具在测试里尤其顺手。比如你要断言返回的标签包含某个值:

func TestTags(t *testing.T) {
	tags := BuildTags(Article{Title: "Go 入门"})
	if !slices.Contains(tags, "Go") {
		t.Fatalf("tags = %v, want Go", tags)
	}
}

比较两个切片:

got := []string{"Go", "Backend"}
want := []string{"Go", "Backend"}

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)
}

这比手写两个嵌套循环清楚很多。测试代码也是代码,能用标准库表达意图时,就不用在每个包里维护一堆 assertStringSliceEqual

不过测试里也要注意错误消息。slices.Equal 只告诉你是否相等,失败时最好把 got 和 want 都打印出来。测试失败信息越清楚,后面修问题越快。

小结

slicesmaps 让现代 Go 写集合操作更方便。克隆用 slices.Clonemaps.Clone,查找用 slices.Containsslices.Index,排序用 slices.Sortslices.SortFunc,删除范围用 slices.Delete

这些工具能减少重复,但不会替你做业务判断。大数据频繁查找仍然要考虑 map,稳定输出仍然要排序 key,复杂过滤有时普通循环更清楚。标准库工具的最好用法,是让意图更明确,而不是把代码写成难懂的链式技巧。

继续阅读

探索更多技术文章

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

全部文章 返回首页