本文永久链接 – https://tonybai.com/2026/11/06/go-generic-set-proposal-accepted
大家好,我是Tony Bai。
【导读】
在 Go 中,如果想表达一个集合(Set),开发者几乎都会不约而同地写出 map[T]struct{}。它简单、高效,却始终不是一个真正意义上的集合类型。
这个延续了十多年的 Go 编程习惯,终于迎来了改变的契机。
2026 年 11 月 4 日,Go 团队正式接受了泛型 Set 提案 #69230。一个基于泛型、支持 Go 迭代器、可以直接用于 range 的 Set 类型,即将进入 Go 扩展库的实验性实现阶段。
这背后,还有一段长达十余年的 Go Set 演进史。
【文章要点】
- 为什么 Go 开发者一直用
map[T]struct{}模拟 Set? - Go 社区关于 Set 的讨论,实际上已经持续了十多年。
- 被接受的泛型 Set 提案提供了哪些 API?
- 为什么新 Set 仍然基于 map 实现,而不是重新造一个数据结构?
iter.Seq和 Go 1.23 引入的 range over function 如何改变 Set 的迭代体验?- 这次提案被接受,对 Go API 设计和生态意味着什么?

如果你是一名 Go 程序员,下面这段代码一定不会陌生:
set := make(map[string]struct{}) set["Go"] = struct{}{} set["Rust"] = struct{}{} set["Python"] = struct{}{} if _, ok := set["Go"]; ok { fmt.Println("Go is in the set") } 在 Go 中,想表达一个集合(Set),最常见的办法就是使用 map[T]struct{}。
为什么是 struct{}?
因为它是一个零大小类型,作为 map 的 value 不需要为每个元素存储实际数据。我们真正需要的只有 map 的 key:存在就是集合成员,不存在就不是。
这个技巧简洁、高效,也几乎成了 Go 程序员的肌肉记忆。
但问题是:Go 有泛型,有 map,有 slice,却一直没有一个统一的 Set 类型。
现在,这个局面终于要改变了。
2026 年 11 月 4 日,Go 团队正式将 #69230 这一泛型 Set 提案的状态从 Likely Accept 更新为 Accepted。
这意味着,Go 社区期待已久的泛型 Set,正式通过了提案评审。
不过,先别急着把所有 map[string]struct{} 替换掉。我们先看看这次提案究竟带来了什么。
一场持续十多年的 Set 之争
Go 对 Set 的讨论,其实远比很多人想象的要早。
我们简单回顾一下这段历史。

2014 年,Go 社区就有人提出直接在语言层面增加 Set 类型。
当时 Go 还没有泛型。Go 团队的回应很有代表性:如果未来 Go 支持泛型,开发者完全可以基于 map 自己定义 Set;如果没有泛型,也没有必要为一个与 map 高度相似的结构增加新的语言类型。
2017 年,社区再次提出 Set 方案,其中还涉及专门为 Set 设计的迭代语法。
2021 年,Go 泛型终于落地,但 Set 的讨论又遇到了一个新问题:Go 应该采用什么样的迭代器协议?
直到 Go 1.23 引入 range over function,迭代器终于有了统一的语言支持。
2024 年 9 月,开发者 Lucas De Angelis 提出了 #69230,重新推动泛型 Set 进入 Go 的生态体系。
此后,Go 团队和 Collections 工作组持续讨论 API 设计,并在 2026 年进一步完善了方案。
从 2014 年到 2026 年,一个看似简单的集合类型,走过了十二年。
这其实很 Go:不急于增加一个新抽象,而是等待语言基础设施成熟后,再寻找合适的设计。
新的 Set 到底长什么样?
这次提案最终确定的实验性包路径是:
golang.org/x/exp/collection/set 它最核心的定义只有一行:
type Set[E comparable] map[E]struct{} 没错!底层仍然是我们熟悉的 map[E]struct{}。
但这一次,它拥有了一个真正的泛型类型名称:Set[E]。
这意味着,开发者不必再自己定义集合类型,也不必在项目中反复实现那些重复的集合操作。
先看一个简单例子:
package main import ( "fmt" "golang.org/x/exp/collection/set" ) func main() { x := set.Of(1, 2, 3) y := set.Of(3, 4, 5) z := x.Intersection(y) fmt.Println(z) // {3} } 是不是清爽了很多?
不再需要手动遍历两个 map,也不必自己实现交集算法。
而且,Set 不仅仅支持交集,还提供了完整的集合操作。
| API | 功能 |
|---|---|
Of | 根据元素创建 Set |
Collect | 从迭代器收集元素 |
Insert | 添加元素 |
Delete | 删除元素 |
Contains | 判断元素是否存在 |
Len | 获取元素数量 |
Clear | 清空集合 |
Clone | 克隆集合 |
Equal | 判断两个集合是否相等 |
Union | 并集 |
Intersection | 交集 |
Difference | 差集 |
SymmetricDifference | 对称差集 |
All | 获取元素迭代器 |
其中,UnionWith、IntersectionWith、DifferenceWith 等方法还支持直接修改原集合,避免额外创建一个新集合。
需要注意的是,这个 API 仍处于实验性阶段,后续可能继续调整。
最值得关注的变化:Set 可以直接 range 了!
如果说泛型 Set 只是给 map 套上了一个类型名称,那么这还不足以构成一个值得关注的变化。
真正有意思的是,它与 Go 新迭代器机制的结合。
Go 1.23 引入 range over function 后,开发者可以使用 iter.Seq 表达一个迭代序列。
Set 正是利用了这一能力:
func (x Set[E]) All() iter.Seq[E] 于是,我们可以直接这样遍历集合:
x := set.Of("Go", "Rust", "Python") for elem := range x.All() { fmt.Println(elem) } 甚至,提案中的 Set 被设计为可以直接作为 range 的对象:
for elem := range x { fmt.Println(elem) } 为什么可以做到?
因为 Set[E] 的底层就是 map,而 Go 已经支持直接 range map。
更重要的是,All() 返回的是 iter.Seq[E],这意味着 Set 可以自然融入 Go 的迭代器生态。
例如,我们可以把其他数据源转换成 Set:
func Collect[E comparable](seq iter.Seq[E]) Set[E] 这让 Set 不再只是一个孤立的数据结构,而是能够与 Go 的迭代器、切片、映射等数据处理方式组合起来。
为什么不直接实现一个全新的 Set?
这里有一个值得关注的设计细节。
Go 团队并没有重新发明一种底层集合数据结构,而是选择让:
type Set[E comparable] map[E]struct{} 成为 Set 的底层表示。
这样做有几个好处。
第一,性能和内存特征保持一致。
开发者熟悉的 map 查找、插入和删除机制依然存在,不需要引入另一套复杂的数据结构。
第二,兼容 Go 已有的语言能力。
Set 可以直接使用 range,也可以利用 map 已有的操作机制。
第三,类型表达更加清晰。
当一个公共 API 需要接收集合时:
func ProcessTags(tags set.Set[string]) 显然比下面这种写法更容易理解:
func ProcessTags(tags map[string]struct{}) 前者表达的是业务语义:这是一个字符串集合。
后者表达的则是实现细节:这是一个 value 为空结构体的 map。
这也是此次提案特别值得关注的地方:Go 团队希望为公共 API 建立一种规范化的 Set 类型表达。
当然,这并不意味着 map[string]struct{} 会从 Go 中消失。相反,它仍然是 Set 的底层实现。
还有一个细节值得注意:最终采用的是 map 类型定义,而不是早期讨论中的 struct 封装。因此,不能简单认为它具备早期方案所设想的零值可直接写入特性。使用时应通过 set.Of() 等构造方式创建可写集合。
Accepted,不等于已经进入标准库
这里还需要给大家一个重要提醒。
此次提案被接受,并不意味着 Go 标准库已经增加了 Set 类型。
前面提到,当前阶段确定的包路径是:
golang.org/x/exp/collection/set 它属于 Go 扩展实验性项目 golang.org/x/exp。
从提案中的包文档来看,Go 团队将其定位为一种实验性原型,目标是探索未来公共 API 中推荐采用的标准 Set 表达方式。
换句话说,这次 Accepted 的意义是:Go 团队已经认可了这套设计方向,接下来将推进具体实现。
至于它最终是否进入标准库,以及何时进入,还需要后续的发展。
小结:一个小类型,为什么值得 Go 社区关注?
从表面看,这次提案似乎只是给 map[T]struct{} 增加了一个泛型类型名称。
但我认为,它的价值远不止于此。
Go 泛型发布后,社区一直在思考一个问题:哪些过去需要依赖第三方库或者手写代码实现的数据结构,值得拥有统一的官方抽象?
Set 是一个很好的答案。
它不像 slice、map 那样属于语言基础设施,但它又是如此常见,以至于几乎每个中大型 Go 项目都会出现自己的 Set 实现。
如今,随着泛型和迭代器机制逐渐成熟,Go 团队终于可以把这些分散的实践收敛起来。
更重要的是,这次设计释放了一个信号:
Go 团队正在尝试为常见的数据结构建立更加统一的 API 约定(与这次 Set 提案同时被接受的,还有其他几个 Collection 类型),而不只是不断向标准库中添加新的类型。
当然,map[string]struct{} 不会真的消失。它依然是这个 Set 的底层实现。
但未来,当我们在设计 Go 公共 API 时,也许可以越来越自然地写出:
func Process[T comparable](items set.Set[T]) { // ... } 而不必再向调用者解释:这是一个用空结构体作为 value 的 map。
十二年的等待,一个小小的 Set,终于有了被 Go 团队认可的设计方向。
这或许就是 Go 语言演进最有意思的地方:有些改变看起来很小,但它们让整个生态的表达方式变得更加统一。
再见,手写 Set 的重复劳动!
参考资料:
- Go Proposal #69230:golang.org/x/exp/collection/set - https://github.com/golang/go/issues/69230
- Go Proposal Review Meeting:2026 年 11 月 4 日接受提案的记录 - https://github.com/golang/go/issues/33502#issuecomment-6642621697
- Go Discussion #47331:关于 Go Set 类型的早期讨论 - https://github.com/golang/go/discussions/47331
还在为写 Agent 框架频频死循环、上下文爆炸而束手无策?我的新专栏 《从0 开始构建 Agent Harness》 将带你:
- 抛弃臃肿框架,回归“驾驭工程 (Harness Engineering)”的第一性原理
- 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等,复刻极简OpenClaw
- 构建坚不可摧的 Safety Middleware 与飞书人工审批防线
- 在底层实现 Token 成本审计、链路追踪与自动化跑分评估
- 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师”
扫描下方二维码,开启从 0 开始构建Agent Harness 的实战之旅。

你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?
- 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
- 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
- 想打造生产级的Go服务,却在工程化实践中屡屡受挫?
继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!
我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。
目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

