Go 1.28 集合类型提案 #80590 深度解读
一、十六年之痛:为什么 Go 需要原生 Set
2009 年 Go 公开发布,2012 年 Go 1.0 上线,到 2026 年 Go 1.27 稳定版已经走过了 17 个年头。但直到今天,Go 标准库依然没有一个真正的集合(Set)类型。想要做"元素是否属于某集合"、"交集/并集/差集"这类操作,Go 程序员的标准答案永远是:
// 经典的 map[T]struct{} 手搓方案
set := make(map[string]struct{})
set["go"] = struct{}{}
set["rust"] = struct{}{}
if _, ok := set["go"]; ok {
fmt.Println("hit")
}这种写法可以工作,但有三大问题:
- 类型不表达意图:
map[string]struct{}是一个 Map,不是 Set,IDE 提示、文档、linter 都无法识别"集合"语义。 - 没有操作符语义:交集、并集、差集需要写 for 循环 + map 操作,没有
intersect/union/difference一类方法。 - 键类型受限:内置 map 强制 key 必须是 comparable(可比较),无法把"切片"、"map"等不可比较类型作为 key。
2025 年底 Go Collections Working Group 正式成立,并把成果以提案 #80590 的形式提交,随 Go 1.28 落地(按 release 时间线,Go 1.28 计划 2026 年底 freeze、2027 年 2 月发布)。
GopherUK 2026 大会上,《Fluent Python》作者 Luciano Ramalho 以"Sets in Modern Go"为题做了完整解读,本文结合这次演讲拆解提案要点,并给出实战代码。
二、提案 #80590 一览:一次性补齐 7 项新增
| 新增 | 作用 | 落地形式 |
|---|---|---|
maphash.Hasher | 自定义 hash/equal 接口 | interface(Go 1.27 已落地) |
container/hash.Map | 基于 Hasher 的泛型 Map | struct |
container/hash.Set | 基于 Hasher 的泛型 Set | struct |
container/heap | 重写版堆,DX 更好 | package |
container/set.Set | "标准 Set",map[E]struct{} 的具名类型 | struct |
container/mapset | 低阶包,暴露函数供存量代码复用 | package |
| 有序 Map | 保留插入顺序 | struct |
这套 API 的设计语言遵循 Go 一贯的"实用 + 简洁"原则,同时给 AI 编程助手一个精准的词汇表——"intersection"、"subset"、"difference" 这些词比"写一个嵌套循环"更利于 LLM 给出正确实现。
三、Set 代数:让 AI 编程助手听得懂你
Ramalho 在演讲一开始举了一个几乎所有后端程序员都写过的场景:
需求:如果一个商品描述里,包含了搜索关键词里的所有词,就把商品展示出来。
传统实现:
func matchesAll(product, keywords []string) bool {
for _, kw := range keywords {
found := false
for _, w := range product {
if w == kw {
found = true
break
}
}
if !found {
return false
}
}
return true
}嵌套循环 + 多重 break + return,圈复杂度直接爆表。
如果用 1.28 的 container/set.Set,同样的逻辑是一次子集判定:
import "container/set"
func matchesAll(product, keywords []string) bool {
p := set.Of(product...)
k := set.Of(keywords...)
return p.IsSupersetOf(k) // p 包含 k 的所有元素
}一行代码,意图清晰,LLM 看一眼就知道要做什么。
再比如"把已经在购物车的商品从收藏夹里去掉",传统写法是双层 for + 删除切片,这就是经典的差集操作:
import "container/set"
favorites := set.Of("A", "B", "C", "D")
inCart := set.Of("C", "D")
remaining := favorites.Difference(inCart) // {A, B}设计哲学:Set 的方法命名直接照搬数学——
Intersection、Union、Difference、SymmetricDifference,把"集合代数"(set algebra)当成 API 的第一公民。
四、API 设计细节:mutate 与 immutable 的命名约定
提案里有一个细节很值得品味:可变方法用动词原名,不可变变体用 With 后缀。
// 可变:直接改集合
s.Insert("go")
s.Remove("rust")
// 不可变:返回新集合
s2 := s.IntersectionWith(other) // s 不变
s3 := s.DifferenceWith(other)这个约定来自 Google 内部对 Go 代码的审计经验——把"我修改了什么"和"我生成了什么"在词法上就分开,code review 一眼能看出副作用。
更贴心的是,可变方法返回 bool,表示操作是否真的改变了集合:
s := set.Of("a", "b")
changed := s.Insert("c") // true
changed = s.Insert("a") // false, 已存在这个返回值让"幂等插入"和"差量同步"算法写起来非常干净。
五、F-bounded多态:让方法返回正确的具体类型
如果你实现过 Java 的 Enum 或者写过递归泛型,会对 F-bounded polymorphism 很熟悉。提案里的 Set 家族用了同一招:
type AbstractSet[E any, S AbstractSet[E, S]] interface {
AbstractCollection[E, S]
// ...
}
type Set[E any] interface {
AbstractSet[E, Set[E]]
// ...
}效果是:s.Intersection(other) 返回的类型和 s 的具体类型一致,而不是退化成 AbstractSet[E, ...]。这意味着你可以一路链式调用,而不需要类型断言。
六、自定义 Hasher:突破"必须可比较"的限制
内置 map 强制 key 必须是 comparable,且只能用 Go 运行时内置的 hash 函数。maphash.Hasher 打破了这两个限制。
import "hash/maphash"
// 1.27 起 Hasher 已落地
type CaseInsensitiveHasher struct{}
func (CaseInsensitiveHasher) Hash(s string) uint64 {
return maphash.String(strings.ToLower(s))
}
func (CaseInsensitiveHasher) Equal(a, b string) bool {
return strings.EqualFold(a, b)
}
// 用自定义 Hasher 构造一个不区分大小写的字符串 Set
import "container/hash"
s := hash.NewSetstring
s.Insert("Go")
fmt.Println(s.Contains("go")) // true威力在哪? 你可以把任何类型作为 Set/Map 的 key——只要你能写 Hash 和 Equal:
- 大小写不敏感的字符串
- 浮点数(带 NaN 处理)
- 自定义结构体(按业务字段比较)
- 切片(把切片内容作为 key)
- 不可比较的网络包(按 payload hash 比较)
Ramalho 在演讲中也指出了这个例子的一个瑕疵:strings.ToLower 是 ASCII-only,而 strings.EqualFold 是 Unicode-aware。两者应该用同一种 normalization 逻辑才严谨。
七、API 不一致:还在演进的证据
提案里有个细节 Ramalho 专门点了出来——container/set.Of(...) 返回的是具名类型 Set[E],而 container/mapset.Of(...) 返回的是裸类型 map[E]struct{}。
// container/set
s := set.Of("a", "b") // s 的静态类型是 Set[string]
// container/mapset
m := mapset.Of("a", "b") // m 的静态类型是 map[string]struct{}这表明 API 还在演进:set 包是"面向未来的标准",mapset 是"给老代码用的兼容垫片"。新代码应该用 set 家族,老代码可以渐进迁移到 mapset。
八、实战:商品搜索的 Set 化重构
把第三节的需求扩成一个完整可运行的例子。Go 1.27 已经支持泛型方法(见 Go 1.27 泛型方法系列),但容器包需要 1.28 才会包含。下面假设你装了 tip 或 1.28 beta:
// 文件:search.go
//go:build goexperiment.collections
package main
import (
"container/set"
"fmt"
"strings"
)
type Product struct {
ID string
Description string
}
func (p Product) Words() set.Set[string] {
return set.Of(strings.Fields(strings.ToLower(p.Description))...)
}
// 搜索:所有关键词都必须出现在商品描述里
func Search(products []Product, keywords []string) []Product {
kw := set.Of(keywords...)
out := make([]Product, 0, len(products))
for _, p := range products {
if p.Words().IsSupersetOf(kw) {
out = append(out, p)
}
}
return out
}
func main() {
products := []Product{
{"1", "Go 1.28 introduces native Set and Hash"},
{"2", "Python 3.13 free-threaded mode is stable"},
{"3", "Rust 1.97 stabilizes async closures"},
}
hits := Search(products, []string{"go", "set"})
for _, h := range hits {
fmt.Println(h.ID, h.Description)
}
// 输出:1 Go 1.28 introduces native Set and Hash
}如果还没有 1.28,可以用 Go 1.27 + 自定义泛型 Set 模拟:
// compat.go
package main
type Set[T comparable] map[T]struct{}
func NewSet[T comparable](items ...T) Set[T] {
s := make(Set[T], len(items))
for _, x := range items {
s[x] = struct{}{}
}
return s
}
func (s Set[T]) Contains(x T) bool {
_, ok := s[x]
return ok
}
func (s Set[T]) IsSupersetOf(other Set[T]) bool {
for x := range other {
if !s.Contains(x) {
return false
}
}
return true
}九、对开发者和 AI 助手意味着什么
Ramalho 在演讲里强调了一句话:
"集合代数提供了一种精确的、与语言无关的词汇。即使在没有原生 Set 的语言里,把问题描述成 'intersection'、'difference'、'subset',也能让人类和 AI 写出更高效的代码。"
对开发者:
- 业务代码里到处是集合操作——权限、标签、去重、推荐——原生 Set 让"业务逻辑"和"实现细节"分开。
- F-bounded 多态 + 具名类型 = 链式 API 不会断类型。
- 自定义 Hasher 打开了"非 comparable 类型做 key"的大门。
对 AI 编程助手:
- 训练语料里"求两个集合的交集"对应很多成熟实现,模型知道该输出什么。
- "intersection" / "subset" 是跨语言一致的概念,比"for 循环 + break"更容易在多语言场景迁移。
- 命名约定清晰(动词原名 vs
With后缀),LLM 写出错误副作用的概率下降。
Ramalho 给初学者的建议是:学好 SQL——SQL 本质上是一种 set-oriented 语言,集合代数是关系代数的核心。
十、迁移指南
- 新代码:直接用
container/set和container/hash家族。 - 老代码:先用
container/mapset包,享受 Set API,零侵入。 - key 不可比较的场景:用
container/hash.Set+ 自定义Hasher,比如把网络包、切片放进集合。 - CI 加 vet 检查:未来 vet 会提示你把
map[T]struct{}改成set.Of(...)形式(提案讨论中)。
参考资料
- Go Collections Working Group 提案 #80590:https://github.com/golang/go/issues/80590
- GopherUK 2026:Luciano Ramalho, "Sets in Modern Go"
- maphash.Hasher 提案(Go 1.27 已落地):https://github.com/golang/go/issues/71470
- The Go Programming Language(Alan A. A. Donovan, Brian W. Kernighan)— bitset 实现
- FastHTTP 项目对 Go 容器类型的演进讨论:https://github.com/golang/go/discussions/63393
- 内部 Reddit 讨论:https://www.reddit.com/r/golang/comments/1cy3a2e/proposal_add_built_in_set_type/
写在最后
Go 用了十六年才"补"上集合类型,看着慢,但回头看——为了让 API 既保持 Go 的简洁,又能给 AI 编程时代留出精确的语义词汇,这种"宁可不做也不做坏"的克制,是 Go 团队一直以来的态度。
1.28 落地后,建议所有 Go 项目先在内部工具里试点,观察 API 稳定性、benchmark 性能,再考虑大规模替换。提案还在演进,container/set 和 container/mapset 命名不一致就是证据——但方向已经明确:Go 终于有了 Set,而这一次是给所有人用的。

