golang.org/x/exp/collection/set: new package with a generic set type
要約
概要
集合(set)を表すジェネリック型 Set[E] を、実験用パッケージ golang.org/x/exp/collection/set に追加する提案です。中身は map[E]struct{} そのもので、Go APIで集合を受け渡すときの標準的な型として試験的に導入されます。
ステータス変更
active → likely_accept
2026年10月7日に @jba が最終版のAPIを提示しました。その翌日にactiveとなり、10月29日に @aclements がproposal review groupを代表してlikely acceptを宣言しています。同日の指摘はドキュメントの書き方に関するものだけでした。「All の順序は "likely random" ではなく非決定的と書くべき」「String の並び順は fmt を引用するだけでなく具体的に書くべき」という2点で、API設計そのものへの反対はありませんでした。この提案は、2025年末に発足したCollections working group(@jba、@adonovan、@griesemer、@mvdan、@rogpeppe、@khr、@ianlancetaylor)がまとめたコレクション関連提案(#80590)の一部です。グループ内で設計がすでに練られていたことが、短期間で合意に至った理由と考えられます。
技術的背景
現状の問題点
標準ライブラリには集合型がなく、map[T]bool か map[T]struct{} で代用するのが慣習です。和集合や積集合のような基本操作も、毎回 make とループで手書きする必要があります。パッケージごとに表現が異なるため、公開APIで集合を受け渡す共通の型もありません。
提案された解決策
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]
メソッドは次のとおりです。
- 基本操作:
All,Clear,Clone,Contains,Delete,DeleteFunc,Equal,Insert,Intersects,Len,String - 新しい集合を返す集合演算:
Union,Intersection,Difference,SymmetricDifference - レシーバ自身を書き換える集合演算:
UnionWith,IntersectionWith,DifferenceWith,SymmetricDifferenceWith
InsertとDeleteは、要素数が変わったかどうかをboolで返します。
型定義が map そのものなので、len(s)、_, ok := s[e]、for e := range sがそのまま使え、map[E]struct{}とも相互に代入できます。Len・Contains・Allは組み込み操作と重複しますが、抽象的な集合を受け取るジェネリック関数にSetを渡せるようにするために用意されています。
これによって何ができるようになるか
- 2つのスライスに共通要素があるかを、O(n*m) の二重ループを使わずに簡潔に判定できる
- 公開APIで「文字列の集合」などを受け渡すときに、共通の型として使える
- 既存の
map[E]struct{}を変換コストなしで集合として扱える
コード例
// 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
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)
}
議論のハイライト
- 当初案(2024年)と @ianlancetaylor の案は、map を内部に隠す struct 型でした。この方式では値レシーバとポインタレシーバのどちらにするかが問題になりました。最終的にworking groupが、map をそのまま型にする「透過的」な定義を採用しました。参照型としての振る舞いが map と同じなので直感的だ、というのが理由です。
ContainsFuncは却下されました。All()をループで回すのと計算量が変わらないためです。一方DeleteFuncは、走査と削除をまとめて効率化できるので採用されました。InsertIf(bool, E)の要望もありましたが、外部のヘルパーで簡単に書けるとして採用されていません。- 置き場所は
container/set→golang.org/x/container/sets→x/exp/collection/setと変遷しました。その途中でContainsAll、InsertAll、DeleteAllが削除され、一時的にSetOf/CollectSetと改名された関数名はOf/Collectに戻されました。 - 比較できない要素型(comparable でない型)向けのハッシュベースの集合は #80584 として、既存の map 型に集合操作を提供するヘルパー関数は #77052 として、別の提案に分けられています。
- @jimmyfrasche は、
Stringが利用している fmt の map ソートロジックを公開して、サードパーティのコレクションでも使えるようにすべきだと提起しました。