Menu

Go 集合 Set:map[T]struct{}、集合运算与泛型 Set

Go 没有内置的集合类型。标准的惯用写法是值为空结构体的 map。学习添加、查找和删除元素,并集、交集和差集,以及如何编写一个小巧的泛型 Set。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

惯用写法:值为空的 map

Go 没有 set 关键字,标准库中也没有集合。一个以元素为键、值不携带任何内容的 map 就能完成这项工作。

struct{} 是空结构体类型,struct{}{} 是它唯一的值。它占零字节,所以这个 map 只存储键,别的什么都不存。

map 的所有规则在这里都适用:键必须是可比较的,遍历顺序是随机的,向 nil map 写入会 panic,并发写入需要加锁。map 页面逐一介绍了这些规则。

map[T]struct{} 还是 map[T]bool

另一种常见写法是 map[T]bool。它读起来更顺,因为不存在的键会返回 false:

seen := map[string]bool{}
seen["a"] = true
if seen["a"] { ... }

两者的取舍:

map[T]struct{}map[T]bool
值的大小0 字节1 字节(另加对齐)
判断是否存在_, ok := s[k]s[k]
歧义没有s[k] = false 是第三种状态

第三行才是很多代码库偏爱空结构体的真正原因:用 bool 的话,迟早会有人写出 s[k] = false,于是 len(s) 就不再是成员的数量。对于小集合,内存上的差别无关紧要。

去除 slice 中的重复元素

集合最常见的用途就是去重。下面的写法保留每个值第一次出现的位置,并保持原有顺序:

输出:

[b a c]
[3 1 2]
[1 2 3]

并集、交集、差集

集合运算只需要几个循环。在另一个集合中判断是否存在时,遍历较小的那个集合,因为每次查找平均都是常数时间。

sorted 只是为了让输出稳定。通过 range 遍历来打印集合,每次运行的顺序都不同。maps.Keys 和 slices.Sorted 需要 Go 1.23。

像 type set map[string]struct{} 这样的命名类型仍然是 map:你可以用同样的方式对它索引、range 遍历和 delete,还可以给它定义方法。

一个小巧的泛型 Set

有了泛型(Go 1.18),一个类型就能覆盖所有可比较的元素类型。对大多数程序来说,这已经足够了:

把 map 包装在结构体中,可以隐藏 struct{}{} 这样的噪音,并保证 map 由构造函数创建,从而避免 nil map 引发的 panic。Sorted 不能是方法:方法不能声明自己的类型参数,也不能收紧类型的 comparable 约束,而排序需要 cmp.Ordered。泛型页面讲解了约束。

如果你需要功能完整的集合(线程安全的版本、丰富的操作),可以使用 github.com/deckarep/golang-set 这样的第三方包。对于大多数代码,Go 程序员用的就是 map 惯用写法,或者像这样一个 30 行左右的类型。

结构体集合

任何可比较的类型都可以作为元素,包括字段都可比较的结构体。这让“我是否见过这一对”这类检查变得很直接:

type edge struct{ from, to string }
visited := map[edge]struct{}{}
visited[edge{"a", "b"}] = struct{}{}

slice 和 map 不能作为集合元素。要跟踪不重复的 slice,先把每个 slice 转换成可比较的键,例如固定大小的数组,或用 fmt.Sprint 构建的字符串。

常见错误

  • 忘记初始化。 var s map[string]struct{} 是 nil;第一次添加就会 panic。
  • 打印集合并期望输出稳定。 先对成员排序。
  • 使用 map[T]bool 并存入 false。 这样 len 就不再是成员的数量。删除元素要用 delete。

常见问题

Go 有集合类型吗?

没有。标准库中没有集合。惯用的写法是一个值不携带任何信息的 map:map[string]struct{}。添加是 s[k] = struct{}{},判断是否存在是 _, ok := s[k],删除是 delete(s, k),大小是 len(s)。

Go 中实现集合应该用 map[T]bool 还是 map[T]struct{}?

map[T]struct{} 让意图更明确,而且它的值占零字节。map[T]bool 读起来更自然(if seen[x]),因为不存在的键会返回 false。两者都是正确的;内存上的差别只对非常大的集合才有影响。选一种并保持一致。

Go 中如何去除 slice 中的重复元素?

如果要按顺序保留第一次出现的元素,就遍历 slice,用一个 map[T]struct{} 记录见过的值,只追加还没见过的。如果不在乎顺序,就先排序再去掉相邻的重复项:slices.Sort(s); s = slices.Compact(s)。

Coddy 编程语言插图

和 Coddy 一起学编程

开始