Golang 提案:container/:泛型集合类型
Golang proposal: container/: generic collection types

原始链接: https://github.com/golang/go/issues/80590

Go Collections 工作组已针对 Go 1.28 版本提出了一套标准库数据结构方案,强调实用性与简洁性。该提案利用 Go 的泛型和迭代器特性,为以往需要手动实现或缺失的集合类型引入了正式的实现方案。 主要新增内容包括: * **`container/hash`**:新增 `Map` 和 `Set` 类型,使用自定义 `Hasher` 接口,支持不可比较的键类型及专门的哈希逻辑。 * **`container/set`**:标准集合实现,提供 `Union`(并集)和 `Intersection`(交集)等常用操作。 * **`container/mapset`**:用于操作“传统”集合(如 `map[T]struct{}`)的辅助函数。 * **`container/ordered`**:用于范围查询的平衡二叉树映射。 * **`container/heap/v2`**:对现有堆 API 的现代化泛型替换。 为确保一致性,工作组开发了非导出的抽象约束接口,以定义标准集合的行为。设计上采用了注重性能的 API 模式,例如“原地”修改变体(使用 `-With` 后缀)以及完整的返回值,从而减少冗余查找。虽然这些提案专注于当前需求,但工作组预计将根据社区反馈对这些 API 进行迭代,并考虑在未来增加栈或插入顺序映射等功能。

近期 Hacker News 上的一场讨论围绕着一项新的 Go 语言提案展开,该提案旨在引入一个包含泛型集合类型的 `container/` 包。虽然一些开发者欢迎这一举措,认为这是姗姗来迟的现代化更新,提供了集合(set)和类型化堆(typed heap)等实用工具,但该公告也再次引发了关于语言演进的两极化争论。 支持者认为,Go 保守的设计理念依然稳固,因为目前的泛型实现优先考虑了可读性和编译速度,而非语法复杂性。相反,批评者则认为在 1.0 版本之后增加泛型是一个错误,他们指出 Go 最初的架构(例如无法在外部类型上定义方法)与泛型编程存在冲突,迫使开发者不得不采用繁琐的变通方案。 这场讨论凸显了两者之间的广泛张力:一方珍视 Go 最初对于“YAGNI”(你不会需要它,即拒绝过度设计)简洁性的关注,另一方则认为该语言必须演进才能在现代替代方案的竞争中保持优势。归根结底,这场辩论反映了人们对强大、现代特性的渴望,与可能损害十多年来定义 Go 的独特简洁性之间产生的冲突。
相关文章

原文

Background: The Go Collections working group was formed in late 2025 with the purpose of bringing common collection data structures to the standard library, guided by the familiar Go principles of pragmatism and simplicity. Alphabetically by last name, the group consists of Jonathan Amsterdam (@jba), Alan Donovan (@adonovan), Robert Griesemer (@griesemer), Daniel Martí (@mvdan), Roger Peppe (@rogpeppe), Keith Randall (@khr), and Ian Lance Taylor (@ianlancetaylor). We’ve now reached a point where we’re ready to share our results with the community.

This issue is an umbrella for discussing several related proposals for new collections APIs for Go 1.28. It presents a high level overview of the themes, and links to the various concrete proposals and associated implementation CLs.

Go currently provides few collection types in its library, and from the outset we have emphasized the flexibility of the language’s built-in slice and map types. Of those provided, the most important is the heap, used for priority queues. Even sets are absent; they are conventionally expressed in terms of map[T]bool or map[T]struct{}. Ordered maps and sets based on binary trees are entirely absent.

Since the addition of generics in Go 1.18 and iterators in Go 1.23, it has become possible for library-defined types to achieve comparable ergonomics to built-in types, and for many common operations on slices and maps to be expressed as calls to library functions. This work seeks to add several of the more important data types to the standard library, and to establish conventions for their APIs and those of future additions.

Proposal: The proposed additions include:

  • #70471, CL 657296 (released in go1.27): hash/maphash.Hasher: a standard interface for expressing custom hash functions and equivalence relations for arbitrary data types. These may differ from the compiler-defined ones used by map[K]V, and are useful when the key type is not comparable (such as a slice or map), or when the default comparison yields the wrong result (such as for types.Type values, which need the deep comparison operation types.Identical). Its package docs include an example of its use in a Bloom filter.

  • #69559, CL 612217: container/hash.Map[K,V]: a hash-based Map that uses the custom hash functions mentioned above.

  • #80584, CL 741160: container/hash.Set[T]: a hash-based Set along the same lines.

  • #69230, CL 745441: container/set.Set[T]: a canonical data type for sets whose elements are comparable. It is transparently represented as map[T]struct{} and supports all the usual set operations such as Union and Intersection. It is more convenient than “legacy” sets based on map[T]bool and map[T]struct{}, and avoids ambiguity about potential false values in a map[T]bool. We expect it to become the standard set in most new Go APIs.

  • #77052, CL 724420: container/mapset: a package of helper functions (Union, Intersection, and so on) for conveniently manipulating legacy sets as sets in existing code whose API cannot be changed. These functions are exactly parallel to the methods of set.Set.

  • #60630: container/ordered.Map[K,V]: an ordered mapping. The current implementation uses a balanced binary tree, but nothing in the design requires that. The common Go pattern of building a map[K]V then sorting its keys performs well in most cases, but on occasion, such as when a range query is needed, other data structures perform much better.

  • #77397: container/heap/v2.Heap: a generic binary heap API to replace the standard library's existing heap, which can be difficult to use.

We expect to consider additional proposals in due course, such as insertion-ordered hash maps (#80194) and stacks.

The initial implementations of all the proposed data structures aim to satisfy the API and asymptotic performance expectations as simply as possible. There are doubtless many opportunities for later optimizations to reduce constant factors, but they are out of scope of the proposal process.

Though the new packages will live in the existing container tree, we prefer the term “collection” to avoid confusion with the container virtualization concept from Linux.

Abstract collection constraint interfaces

Most of the methods of the new Map and Set types are not particular to any concrete representation type, but are common across all Maps and Sets. However, they are not really implementions of a common interface type because of the “binary method problem”: if each set data type S has a Union method of the form func (S) Union(S) S, then the Union methods of different set types are incompatible, so they have no common ordinary interface. To express this abstract Set type, we must use F-bounded polymorphism, or recursive constraint interfaces.

CL 761460 adds to the container package unexported abstract Collection, Set, and Map constraint interface types that permit package implementors to write abstract helper functions (such as ContainsAny, Subset, or Arbitrary) that work across a range of concrete collection, set or map types. We reproduce these interfaces below, with some brief commentary, to help give a high-level picture but they are not part of any proposal. They merely serve to guarantee conformance in tests. See the individual proposals for more detail.

// _AbstractCollection models a collection C of elements E,
// such as *hash.Map, *hash.Set, *ordered.Map, or set.Set.  
type _AbstractCollection[E any, C _AbstractCollection[E, C]] interface {  
	Clear()  
	Clone() C  
	Contains(E) bool  
	ContainsAll(iter.Seq[E]) bool  
	Len() int  
	String() string  
}

// _AbstractMap models a mapping M from keys K to values V,  
// such as *hash.Map or *ordered.Map.  
type _AbstractMap[K, V any, M _AbstractMap[K, V, M]] interface {  
	_AbstractCollection[K, M]

	All() iter.Seq2[K, V]  
	At(K) V  
	Delete(K) (V, bool)  
	DeleteAll(iter.Seq[K]) bool  
	DeleteFunc(func(K, V) bool) bool  
	Get(K) (V, bool)  
	Keys() iter.Seq[K]  
	Set(K, V) (V, bool)  
	SetAll(iter.Seq2[K, V]) bool  
	Values() iter.Seq[V]  
}

// _AbstractSet models a set S of elements E,  
// such as *hash.Set, or set.Set.  
type _AbstractSet[E any, S _AbstractSet[E, S]] interface {  
	_AbstractCollection[E, S]

	All() iter.Seq[E]  
	Delete(E) bool  
	DeleteAll(iter.Seq[E]) bool  
	DeleteFunc(func(E) bool) bool  
	Difference(S) S  
	DifferenceWith(S)  
	Equal(S) bool  
	Insert(E) bool  
	InsertAll(iter.Seq[E]) bool  
	Intersection(S) S  
	IntersectionWith(S)  
	Intersects(S) bool  
	SymmetricDifference(S) S  
	SymmetricDifferenceWith(S)  
	Union(S) S  
	UnionWith(S)  
}

For now these abstract types are non-exported and merely serve as documentation of Go’s conventions to help ensure consistency. We do not propose to publish them yet, but may do so a later release after gaining experience with the concrete collection types. In the meantime, users can define minimal constraint types as needed, as in this example (from CL 761460) of a generic Take function over abstract sets:

// _TakeSet defines an abstraction of a set sufficient for the [Take] function.  
type _TakeSet[E any, S _TakeSet[E, S]] interface {  
	All() iter.Seq[E]  
	Delete(E) bool  
}

// Take removes and returns an arbitrary element from a set.  
// It returns zero if the set was empty.  
func Take[S _TakeSet[E, S], E any](set S) (e E, found bool) {  
	for e = range set.All() {  
		found = true  
		set.Delete(e)  
		break  
	}  
	return  
}

There is a certain arbitrariness to the set of methods included in each interface. For some data structures, a method permits a more efficient specialized implementation. But if every possible operation were added to the interface, the burden on the implementor would be unreasonable.

For instance, should the Set interface include a Subset(Set) bool method, or should Subset be written as a generic operation over abstract sets, like the Take example? Ordered sets can quickly reject a Subset test when the two operands have disjoint ranges, but even so, in the common case a Subset test is still typically O(n), so we decided to omit Subset from the interface. By contrast, we retained DeleteFunc in the Set and Map interfaces because, without it, conditionally deleting each element of a tree is asymptotically worse: O(n log n) instead of O(n).

By delaying the commitment to a particular set of methods, we can learn from practice. It may turn out that we don’t need to publish canonical constraint types at all.

Miscellaneous rationalizations

The remainder of this doc briefly notes a few of the many small design choices that led to the current set of proposals.

Methods return as much information as possible to avoid repeated lookups. For example:

  • Most mutation methods report whether they changed the size of the collection.
  • Map.Set and Map.Delete return the previous key if any, plus a boolean so that an existing key can be distinguished from the zero value.
  • Get is variant of At that provides the boolean. (At is provided for convenience of use in expressions.)

Map.Set should replace any existing entry with the same key, following the built-in map.

Unlike Sets, maps have no Equal method, because map values may be non-comparable.

The fundamental set operations (Intersects, Union, etc) are part of the Set interface to enable efficient concrete implementations across a variety of representations, even though many of these could be expressed abstractly in terms of just All, Len, and Contains, as shown in this table, at some asymptotic cost in performance:

- Union{,With}               interface{ All() iter.Seq\[E\] }  
- Intersection               interface{ All() iter.Seq\[E\]; Contains(E) bool; Len() int }  
- IntersectionWith           interface{                    Contains(E) bool }  
- Difference                 interface{ All() iter.Seq\[E\]; Contains(E) bool }  
- DifferenceWith             interface{ All() iter.Seq\[E\]; Contains(E) bool }  
- SymmetricDifference{,With} interface{ All() iter.Seq\[E\] }

Secondary operations such as Set.{Take,Arbitrary,Subset,Superset} were removed from the interface and expressed as generic operations using the abstract set interface, again at some potential cost in asymptotic performance. DeleteFunc was retained.

Set algebra operations such as Union are purely functional, returning their result as a new set. Each comes with a -With variant that mutates its left operand and returns no result. The two variants are “convenient” and “allocation efficient”, respectively. We rejected the idea of merging them into a single method based on experience with the math/big.Int API, and to avoid risks of accidental mutation or forgetting to use the result.

It is possible to define a KeySetView[M, K, V] wrapper type that satisfies the Set[K] abstraction using the key set of an underlying Map[K,V] of type M. (Inserting an element to the Set is of course meaningless and must panic.)

For symmetry, let’s consider each of the AbstractMap methods and the operations provided by the existing ‘maps’ package:

  • no maps.Clear: served by builtin clear(m)
  • AbstractMap.Clone = maps.Clone
  • no maps.Contains; served by _, ok = m[k]; but see proposal #67377
  • no maps.ContainsAll: served efficiently by for k := range seq { _, ok = m[k], … }
  • no maps.Len: served by builtin len(m)
  • AbstractMaps.All = maps.All
  • no maps.At: served by m[k]
  • no maps.Delete: served by delete(m, k)
  • no maps.DeleteAll: served efficiently by for k = range seq { delete(m, k) }
  • AbstractMaps.DeleteFunc = maps.DeleteFunc
  • no maps.Get: served by v, ok = m[k]
  • AbstractMaps.Keys = maps.Keys
  • no maps.Set: served by prev, ok = m[k]; m[k] = newval
  • AbstractMap.SetAll = maps.Insert
  • AbstractMaps.Values = maps.Values

Five of them (Clone, DeleteFunc, All, Keys, Values) are exactly parallel. One operation (AbstractMap.SetAll) has a different name (maps.Insert). All the rest are served by built-in operators.
We might want to propose adding maps.{Contains,ContainsAll,DeleteAll}. Contains is more useful than _, ok = s[k] in an expression context; ContainsAll and DeleteAll avoid the need for loops and boolean bookkeeping.

联系我们 contact @ memedata.com