Announcement

👇Official Account👇

Welcome to join the group & private message

Article first/tail QR code

Skip to content

Go 1.28 集合类型提案 #80590 深度解读

一、十六年之痛:为什么 Go 需要原生 Set

2009 年 Go 公开发布,2012 年 Go 1.0 上线,到 2026 年 Go 1.27 稳定版已经走过了 17 个年头。但直到今天,Go 标准库依然没有一个真正的集合(Set)类型。想要做"元素是否属于某集合"、"交集/并集/差集"这类操作,Go 程序员的标准答案永远是:

go
// 经典的 map[T]struct{} 手搓方案
set := make(map[string]struct{})
set["go"] = struct{}{}
set["rust"] = struct{}{}
if _, ok := set["go"]; ok {
    fmt.Println("hit")
}

这种写法可以工作,但有三大问题:

  1. 类型不表达意图map[string]struct{} 是一个 Map,不是 Set,IDE 提示、文档、linter 都无法识别"集合"语义。
  2. 没有操作符语义:交集、并集、差集需要写 for 循环 + map 操作,没有 intersect / union / difference 一类方法。
  3. 键类型受限:内置 map 强制 key 必须是 comparable(可比较),无法把"切片"、"map"等不可比较类型作为 key。

2025 年底 Go Collections Working Group 正式成立,并把成果以提案 #80590 的形式提交,随 Go 1.28 落地(按 release 时间线,Go 1.28 计划 2026 年底 freeze、2027 年 2 月发布)。

提案地址:https://github.com/golang/go/issues/80590

GopherUK 2026 大会上,《Fluent Python》作者 Luciano Ramalho 以"Sets in Modern Go"为题做了完整解读,本文结合这次演讲拆解提案要点,并给出实战代码。

二、提案 #80590 一览:一次性补齐 7 项新增

新增作用落地形式
maphash.Hasher自定义 hash/equal 接口interface(Go 1.27 已落地
container/hash.Map基于 Hasher 的泛型 Mapstruct
container/hash.Set基于 Hasher 的泛型 Setstruct
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 在演讲一开始举了一个几乎所有后端程序员都写过的场景:

需求:如果一个商品描述里,包含了搜索关键词里的所有词,就把商品展示出来。

传统实现:

go
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,同样的逻辑是一次子集判定

go
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 + 删除切片,这就是经典的差集操作

go
import "container/set"

favorites := set.Of("A", "B", "C", "D")
inCart := set.Of("C", "D")
remaining := favorites.Difference(inCart) // {A, B}

设计哲学:Set 的方法命名直接照搬数学——IntersectionUnionDifferenceSymmetricDifference,把"集合代数"(set algebra)当成 API 的第一公民。

四、API 设计细节:mutate 与 immutable 的命名约定

提案里有一个细节很值得品味:可变方法用动词原名,不可变变体用 With 后缀

go
// 可变:直接改集合
s.Insert("go")
s.Remove("rust")

// 不可变:返回新集合
s2 := s.IntersectionWith(other) // s 不变
s3 := s.DifferenceWith(other)

这个约定来自 Google 内部对 Go 代码的审计经验——把"我修改了什么"和"我生成了什么"在词法上就分开,code review 一眼能看出副作用。

更贴心的是,可变方法返回 bool,表示操作是否真的改变了集合:

go
s := set.Of("a", "b")
changed := s.Insert("c") // true
changed = s.Insert("a")  // false, 已存在

这个返回值让"幂等插入"和"差量同步"算法写起来非常干净。

五、F-bounded多态:让方法返回正确的具体类型

如果你实现过 Java 的 Enum 或者写过递归泛型,会对 F-bounded polymorphism 很熟悉。提案里的 Set 家族用了同一招:

go
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 打破了这两个限制。

go
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——只要你能写 HashEqual

  • 大小写不敏感的字符串
  • 浮点数(带 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{}

go
// 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:

go
// 文件: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 模拟:

go
// 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 语言,集合代数是关系代数的核心。

十、迁移指南

  1. 新代码:直接用 container/setcontainer/hash 家族。
  2. 老代码:先用 container/mapset 包,享受 Set API,零侵入。
  3. key 不可比较的场景:用 container/hash.Set + 自定义 Hasher,比如把网络包、切片放进集合。
  4. CI 加 vet 检查:未来 vet 会提示你把 map[T]struct{} 改成 set.Of(...) 形式(提案讨论中)。

参考资料

写在最后

Go 用了十六年才"补"上集合类型,看着慢,但回头看——为了让 API 既保持 Go 的简洁,又能给 AI 编程时代留出精确的语义词汇,这种"宁可不做也不做坏"的克制,是 Go 团队一直以来的态度。

1.28 落地后,建议所有 Go 项目先在内部工具里试点,观察 API 稳定性、benchmark 性能,再考虑大规模替换。提案还在演进,container/setcontainer/mapset 命名不一致就是证据——但方向已经明确:Go 终于有了 Set,而这一次是给所有人用的

上次更新于: