“我的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,甚至编写自己的极简实现。
为什么Go的设计者没有在标准库中添加Set?
\n\n答案在于语言的极简主义和实用主义。Go团队(Rob Pike、Ken Thompson、Robert Griesemer)遵循的原则是:“如果可以用一种语言结构完成某件事,就不要创造多余的东西。”在Go中,map不仅仅是一个字典,更是一个通用容器。它已经支持键的唯一性,而这正是集合的主要特性。
“少一些魔法,多一些清晰”的哲学
\n\n与Python不同,Python中set是一个独立的类型,拥有自己的方法(.add()、.union()),而Go决定不使语言复杂化。如果map[T]struct{}能完成同样的功能,为什么还要编写单独的结构呢?这节省了标准库的空间,并降低了初学者的入门门槛。关键是学会如何正确地将map用作set。
如何在Go中模拟Set:三种可行的方法
\n\n让我们进入实践环节。以下是仅使用Go标准功能实现集合的方法。
\n\n1. 最流行的方法:map[type]struct{}
\n\n这是Go社区中的标准方法。为什么用struct{}?因为空结构体不占用内存(0字节)——非常适合作为元素“存在”的标记。
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语法完成。
2. 带方法的自定义类型
\n\n如果您需要干净的API,可以将map封装在结构体中:
\n\npackage 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\n3. 使用bool代替struct{}
\n\n有些人使用map[T]bool,但这会为每个值浪费1字节。在大多数情况下,差异并不明显,但如果您正在编写高负载服务,最好坚持使用struct{}。