Go中没有Set:为什么没有内置集合以及如何应对

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

“我的Set在哪?”——Go开发者的灵魂呐喊

\n\n

如果您是从Python、JavaScript或Java转到Go的,那么迟早会问一个问题:“我亲爱的Set到底在哪?”——这很正常。在Stack Overflow上,“Golang why don't we have a set datastructure”这个话题获得了数千次浏览和数十次热烈讨论。开发者们习惯了拥有唯一元素的便捷集合,而Go只提供了数组、切片(slices)和映射(maps)。在本文中,我们将探讨Go设计者的哲学,学习如何模拟Set,甚至编写自己的极简实现。

\n\n

为什么Go的设计者没有在标准库中添加Set?

\n\n

答案在于语言的极简主义和实用主义。Go团队(Rob Pike、Ken Thompson、Robert Griesemer)遵循的原则是:“如果可以用一种语言结构完成某件事,就不要创造多余的东西。”在Go中,map不仅仅是一个字典,更是一个通用容器。它已经支持键的唯一性,而这正是集合的主要特性。

\n\n

“少一些魔法,多一些清晰”的哲学

\n\n

与Python不同,Python中set是一个独立的类型,拥有自己的方法(.add()、.union()),而Go决定不使语言复杂化。如果map[T]struct{}能完成同样的功能,为什么还要编写单独的结构呢?这节省了标准库的空间,并降低了初学者的入门门槛。关键是学会如何正确地将map用作set。

\n\n

如何在Go中模拟Set:三种可行的方法

\n\n

让我们进入实践环节。以下是仅使用Go标准功能实现集合的方法。

\n\n

1. 最流行的方法:map[type]struct{}

\n\n

这是Go社区中的标准方法。为什么用struct{}?因为空结构体不占用内存(0字节)——非常适合作为元素“存在”的标记。

\n\n
package main\n\nimport "fmt"\n\nfunc main() {\n    // 创建一个整数集合\n    set := make(map[int]struct{})\n\n    // 添加元素\n    set[1] = struct{}{}\n    set[2] = struct{}{}\n    set[3] = struct{}{}\n    set[1] = struct{}{} // 重复项被忽略\n\n    // 检查是否存在\n    if _, exists := set[2]; exists {\n        fmt.Println("2在集合中")\n    }\n\n    // 删除\n    delete(set, 3)\n\n    // 遍历\n    for key := range set {\n        fmt.Println(key)\n    }\n}
\n\n

优点:值为零内存分配,简单。缺点:没有.Add()或.Contains()方法——所有操作都通过map语法完成。

\n\n

2. 带方法的自定义类型

\n\n

如果您需要干净的API,可以将map封装在结构体中:

\n\n
package main\n\nimport "fmt"\n\n// Set — 泛型集合(Go 1.18+)\ntype Set[T comparable] struct {\n    items map[T]struct{}\n}\n\n// NewSet 创建一个新的Set\nfunc NewSet[T comparable]() *Set[T] {\n    return &Set[T]{items: make(map[T]struct{})}\n}\n\n// Add 添加元素\nfunc (s *Set[T]) Add(item T) {\n    s.items[item] = struct{}{}\n}\n\n// Contains 检查是否存在\nfunc (s *Set[T]) Contains(item T) bool {\n    _, ok := s.items[item]\n    return ok\n}\n\n// Size 返回大小\nfunc (s *Set[T]) Size() int {\n    return len(s.items)\n}\n\nfunc main() {\n    intSet := NewSet[int]()\n    intSet.Add(10)\n    intSet.Add(20)\n    fmt.Println(intSet.Contains(10)) // true\n    fmt.Println(intSet.Size())       // 2\n}
\n\n

这已经类似于一个完整的Go中的set数据结构。泛型(generics)从1.18版本开始可用,因此该代码适用于现代项目。

\n\n

3. 使用bool代替struct{}

\n\n

有些人使用map[T]bool,但这会为每个值浪费1字节。在大多数情况下,差异并不明显,但如果您正在编写高负载服务,最好坚持使用struct{}。

\n\

博客

书籍推荐