メインコンテンツへスキップ
#69230active

golang.org/x/exp/collection/set: new package with a generic set type

新規提案

要約

AIによる要約であり、誤りを含む場合があります。

概要

Goで、比較可能な(comparable)要素を持つ集合を表す汎用型 Set[E] を、実験用モジュール golang.org/x/exp/collection/set として追加する提案です。目標は、新しく作るGoのAPIで集合を表すときの「標準の型」を決めることです。

ステータス変更

(新規) → active
この提案は2024年9月に出され、長い間コミュニティの議論だけが続いていました。2025年末にGoチームの「Collections working group」が作られました。メンバーは @jba、@adonovan、@griesemer、@mvdan、@rogpeppe、@khr、@ianlancetaylor です。2026年7月、このグループは推奨APIをまとめ、コレクション型全体をまとめる親issue #80590 の一部としてこの提案を位置づけました。
その後APIは何度か直されました。最後の変更(2026年10月7日)で、置き場所は golang.org/x/exp/collection/set、関数名は Of と Collect に戻りました。実装CL 845385 もできています。これを受けて、2026年10月8日に @aclements がactive列に移し、毎週のproposal review meetingで正式に審議されることになりました。

技術的背景

現状の問題点

Goの標準ライブラリには集合型がありません。そのため、普通は map[T]bool か map[T]struct{} で代用しています。この方法には次の問題があります。

  • 和集合・積集合・差集合などを求めるたびに、make とループを手で書く必要がある
  • map[T]bool では、値が false のキーを「集合に含まれる」とみなすかどうかがあいまいになる
  • 公開APIで集合を受け渡すときの共通の型がなく、golang-set、hashicorp/go-set などのライブラリがそれぞれ別の型を使っている

提案された解決策

最新のAPIは、型を map そのものとして定義しています。

package set // import "golang.org/x/exp/collection/set"
type Set[E comparable] map[E]struct{}
func Of[E comparable](elems ...E) Set[E]
func Collect[E comparable](seq iter.Seq[E]) Set[E]
func (x Set[E]) All() iter.Seq[E]
func (s Set[E]) Clear()
func (s Set[E]) Clone() Set[E]
func (x Set[E]) Contains(e E) bool
func (x Set[E]) Delete(e E) bool
func (x Set[E]) DeleteFunc(f func(E) bool) bool
func (x Set[E]) Equal(y Set[E]) bool
func (x Set[E]) Insert(elem E) bool
func (x Set[E]) Intersects(y Set[E]) bool
func (x Set[E]) Len() int
func (x Set[E]) String() string // "{a, ..., z}" 形式。要素はソートされる
func (x Set[E]) Union(y Set[E]) Set[E]
func (x Set[E]) UnionWith(y Set[E])
func (x Set[E]) Intersection(y Set[E]) Set[E]
func (x Set[E]) IntersectionWith(y Set[E])
func (x Set[E]) Difference(y Set[E]) Set[E]
func (x Set[E]) DifferenceWith(y Set[E])
func (x Set[E]) SymmetricDifference(y Set[E]) Set[E]
func (x Set[E]) SymmetricDifferenceWith(y Set[E])
  • 演算は2種類あります。Union のように新しい集合を返す版と、UnionWith のようにレシーバ自身を書き換える版です。
  • Insert と Delete は、集合の要素数が変わったかどうかを bool で返します。

これによって何ができるようになるか

中身は map[E]struct{} なので、len(s)、_, ok := s[e]、for e := range s といったmapの操作がそのまま使えます。一方で、Len、Contains、All といったメソッドもあります。これらは組み込み操作と重複して見えますが、抽象的な集合を受け取るジェネリック関数にこの型を渡せるようにするためのものです。
想定される主な使い道は次の3つです。

  1. 2つのスライスに共通する要素を探す処理を、slices.Contains による O(n*m) の二重ループではなく、集合演算で書ける
  2. 公開APIの引数や戻り値で「重複のない集まり」を型として示せる
  3. slices.Values や maps.Keys で得たイテレータから、Collect で集合を作れる

コード例

// Before: map を集合として手書きで扱う
a := map[string]struct{}{"x": {}, "y": {}}
b := map[string]struct{}{"y": {}, "z": {}}
inter := make(map[string]struct{})
for k := range a {
    if _, ok := b[k]; ok {
        inter[k] = struct{}{}
    }
}
// After: set パッケージを使う
x := set.Of("x", "y")
y := set.Collect(slices.Values([]string{"y", "z"}))
z := x.Intersection(y) // {y}
fmt.Println(z)         // "{y}"
for e := range z {     // map と同じく range で回せる
    println(e)
}

議論のハイライト

  • struct型かmap型か: @ianlancetaylor の当初案は、map をラップする struct を作り、ポインタレシーバのメソッドを持たせるものでした。working groupは最終的に map[E]struct{} をそのまま型として定義する方式を選びました。理由は、mapと同じ参照の振る舞いで分かりやすく、range などの組み込み操作も使えるためです。
  • メソッドの削減: ContainsAll、InsertAll、DeleteAll は削除されました。ContainsFunc も入れていません。@adonovan によると、イテレータと for ループで書いても計算量は変わらないためです。一方 DeleteFunc は、走査と削除をまとめて効率よく行える可能性があるため残されました。
  • 置き場所と名前の変遷: container/set → golang.org/x/container/sets(関数名は SetOf / CollectSet に変更)→ golang.org/x/exp/collection/set(Of / Collect に戻す)と変わりました。最終的には、正式採用前の試作として x/exp に置く形になりました。
  • 関連提案との役割分担:
    • #77052 container/mapset: 既存の map[T]bool などの「旧来の集合」に対する補助関数
    • #80584 hashed.Set: 比較できない要素型や、独自の等価性判定を使う場合のハッシュ集合
    • 本提案: 新しいAPIで使う標準の集合型
  • 却下・保留されたアイデア: set.Set{...} のような複合リテラル構文(言語仕様の変更が必要になる)、直積(Cartesian product)、InsertIf は採用されていません。

関連リンク