Go 标准库到现在都没有一个正经的 Set 类型。想用集合,官方给的答案十几年没变:自己拿 map[T]bool 或 map[T]struct{} 凑一个。
这个状态可能要在 Go 1.28 结束。一个叫 Go Collections 的工作组,最近把一份总括性提案摆上了台面:泛型 Set、自定义哈希 Map、有序 Map、新版 Heap,一次性打包提交。
工作组成立不久,核心成员里能看到 Robert Griesemer、Ian Lance Taylor 这些参与过 Go 语言核心设计的名字。他们等的不是灵感,是语言能力补齐——泛型(Go 1.18)、迭代器(Go 1.23)到位之后,库定义的类型才第一次有可能跟内建类型一样顺手。这次提案,是把这两块拼图正式用起来的第一次系统尝试。
这次到底要加什么
提案覆盖四个独立子提案,外加一份已经落地的基础设施。
| 包名 | 解决什么 | 当前状态 | 谁会用到 |
|---|---|---|---|
hash/maphash.Hasher | 自定义哈希与等价关系的标准接口 | 已发布(Go 1.27) | 需要自定义键比较逻辑的库作者 |
container/hash.Map/Set | 不可比较类型(如切片)做键 | 提案+实现CL,目标 1.28 | 处理复杂键结构的开发者 |
container/set.Set | 可比较元素的标准集合 | 提案+实现CL,目标 1.28 | 大多数日常需要 Set 的场景 |
container/ordered.Map | 有序映射,支持范围查询 | 提案+实现CL,目标 1.28(平衡二叉树实现) | 需要有序遍历、范围查询的场景 |
container/heap/v2.Heap | 重写现有 heap 接口 | 提案+实现CL,目标 1.28 | 用堆做优先队列的开发者 |
除了 maphash.Hasher,其余都还停在提案阶段,目标合入 Go 1.28,不是已经定论。Go 的提案走到这一步,被改甚至被搁置都不稀奇,这次也可能一样。
两处克制,比新增类型更值得读
提案里最扎眼的不是加了什么,是明确写下了不做什么。
第一处:初版只保证 API 正确和渐进复杂度(asymptotic performance),常数级优化明确排除在这轮之外。这批 Set、Map、Heap 先求对,不求快,调优留到以后。
第二处更关键:文档里提到 Collection、Set、Map 三个抽象约束接口,用来解释各个类型的方法为什么长得像。但这三个接口目前不导出,也不属于任何提案。
原因说得很直白:每个 Set 的 Union 方法签名都绑死自己的具体类型,天生互不兼容,这是经典的 binary method problem。要表达一个抽象 Set 概念,得靠递归约束或 F-bounded 多态,一旦公开导出,就是长期维护的公共契约。团队选择先内部用着攒经验,不急着许诺。
这些取舍里能看到具体的历史教训。Set 用 With 后缀区分纯函数版和原地修改版,是从 math/big.Int 早年的问题里学的。DeleteFunc 留在接口里没删,是因为省掉它会让树结构删除从 O(n) 退化成 O(n log n)。这种细节,比“加了个 Set”本身更值得工程师读一遍原文。
谁该关心,现在该做什么
三类人跟这份提案关系最近。
日常写业务代码的 Go 工程师:现在不用改任何东西。提案还在走流程,Go 1.28 没有发布时间表,标准 Set 用上之前,map[T]struct{} 该怎么写还怎么写。
自己包装过 Set、OrderedMap 工具库的库作者:值得盯紧的是那三个未导出的抽象接口。一旦哪天正式公开,就意味着所有实现要对齐同一套方法签名,现在跟进社区讨论,比事后重构划算。
长期靠 golang.org/x/exp 或自建容器包过日子的团队:标准库补齐之后,这类自建方案的必要性会打折扣,但现在还谈不上迁移,先看提案能不能真的落进 Go 1.28。
Go 早年的哲学是语言够灵活,slice 和 map 就够用,这句话说了十几年,几乎成了固定说辞。这次松口,不是哲学变了,是工具终于配齐了——没有泛型和迭代器,硬做一个 Set,只会是个半吊子库类型。真正该盯的不是这几个新包,是那份还没导出的抽象接口最终会不会公开。公开了,Go 才算有一套集合语言,不是几个孤立类型拼在一起。
