golang.org/x/exp/collection/hashed: Set, a set based on a hash table that uses the element type's custom hash function and equivalence relation
要約
概要
キーの比較方法を利用者が決められるハッシュテーブル型の集合 Set[E] を、新しいパッケージ golang.org/x/exp/collection/hashed に追加する提案です。要素の型が == で比較できない場合や、== とは違う「同じとみなす基準」を使いたい場合に使います。
ステータス変更
active → likely_accept
2026-10-08 にレビュー対象(active)になり、2026-10-28 の Proposal Review Meeting で likely accept と判断されました。同じ会議では、コレクション全体をまとめる提案 #80590 と、関連する #69559(hashed.Map)、#69230(collection/set)、#60630(collection/sorted)もそろって likely accept になっています。この提案は、Collections working group が進めるコレクション型をまとめて整備する作業の一部と位置づけられます。議論の中で配置先とAPIが絞り込まれ、大きな反対意見は残りませんでした。@aclements からはドキュメントの細かな指摘(Nit)が1件あっただけです。
技術的背景
現状の問題点
Go の組み込み map で集合を表す場合、要素の型は comparable(== で比較できる型)でなければならず、等しいかどうかの判定にも == しか使えません。そのため、次のような集合は書けません。
- スライスを含む構造体や
go/types.Typeなど、==で比較できない、または==では意味的な等しさを判定できない値の集合 - 大文字小文字を区別しない文字列の集合のように、独自の等価関係(どの値同士を同じとみなすかのルール)を使う集合
これまでは、要素を正規化した文字列などに変換してmap[string]Tのキーにするといった回避策を取る必要がありました。
提案された解決策
maphash.Hasher[E](ハッシュ関数 Hash と等価判定 Equal を組にしたインターフェース)をコンストラクタに渡して Set を作ります。最新のAPIは次のとおりです。
package hashed // golang.org/x/exp/collection/hashed
func NewSet[E any](hasher maphash.Hasher[E]) *Set[E]
// 基本操作: All, Clear, Clone, Contains, Delete, DeleteFunc, Insert, Len, String, Equal
// 集合演算: Union / Intersection / Difference / SymmetricDifference(新しい集合を返す)
// UnionWith / IntersectionWith / DifferenceWith / SymmetricDifferenceWith(s 自体を書き換える)
// Intersects
仕様上の主な制約は次のとおりです。
NewSetで作る必要があります。ゼロ値や nil は使えず、値のコピーも禁止です(複製はCloneを使います)。- comparable な型を通常の
==で扱うなら、組み込みmapかcollection/setを使うよう案内しています。 - NaN を含む浮動小数点数は避けるよう求めています。
e == eが常に成り立つ前提で処理を省略する場合があるためです(例: 空でないsについてIntersects(s, s)は要素を見ずに true を返します)。 - 二項演算に渡す2つの集合は、同じ等価関係を使っていなければなりません。
これによって何ができるようになるか
- 型チェッカーやツール:
go/types.Typeのように構造的に等しいかで比べたい値の集合を扱えます。 - 独自の正規化: 大文字小文字を無視する文字列、末尾の
/を無視するパスなど、独自の等価基準による重複排除ができます。 - 比較できない複合値: スライスを含む構造体などの集合を、内容のハッシュに基づいて扱えます。
- 和集合や差集合といった集合演算がメソッドとして用意されているため、自分でループを書く必要がなくなります。
コード例
// Before: 正規化したキーを使う map で代用する
seen := map[string]string{} // 正規化したキー → 元の値
for _, s := range names {
k := strings.ToLower(s)
if _, ok := seen[k]; !ok {
seen[k] = s
}
}
// After: 等価関係を持つ Hasher を渡して Set を作る
type foldHasher struct{}
func (foldHasher) Hash(h *maphash.Hash, s string) { h.WriteString(strings.ToLower(s)) }
func (foldHasher) Equal(a, b string) bool { return strings.EqualFold(a, b) }
set := hashed.NewSet[string](foldHasher{})
for _, s := range names {
set.Insert(s) // "Go" と "GO" は同じ要素として扱われる
}
other := hashed.NewSet[string](foldHasher{})
other.Insert("go")
fmt.Println(set.Intersects(other), set.Union(other).Len())
(Hash と Equal は、Equal が true になる2つの値に必ず同じハッシュを返すよう実装する必要があります。)
議論のハイライト
- 配置先の変遷: 当初の配置先は標準ライブラリの
container/hash.Setでした。その後x/exp/container/sets.Hashedを経て、最終的にgolang.org/x/exp/collection/hashed.Setになりました。まず x/exp で試す方針がとられています。 - API の削減: 2026-09 の更新で、iterator を受け取る
ContainsAll、InsertAll、DeleteAllが削除されました。 collection/setに統合しない理由: @willfaught は、set.Setに Hasher をオプションとして渡せるようにすればよいのではないかと提案しました。@adonovan は、#69230 のset.Setは組み込み map を中身が見える形でラップする型であり、独自の等価関係が必要なときはhashed.Setを使うという役割分担を説明しました。一部の型だけ通常の比較で済ませたい場合はmaphash.ComparableHasherを使えばよい、とも述べています。- 命名と直列化: @seankhliao は
DifferenceやSymmetricDifferenceといった名前が分かりにくいと指摘しました。@prattmic は JSON などへのマーシャラーを用意すべきではないかと問いかけました。どちらも、提案の変更には至っていません。 - ドキュメントの Nit: @aclements は、「Union や Intersection が必要なら set パッケージのヘルパーを使う」という括弧書きについて、
hashed.Set自体にもそれらのメソッドがあるので紛らわしいと指摘しました。これは組み込み map に対する集合演算の話だと読み取れる表現にすべき、という趣旨です。
関連リンク
- Proposal Issue github.com/golang/go
- Review Comment proposal review meeting
- #70471 hash/maphash: standardize the hash function(Hasher 関連)
- Proposal Issue
- Review Minutes
- #69559 hashed.Map(対になる Map の提案)
- #80590 generic collection types(コレクション全体の提案)
- #69230 collection/set(comparable 型向けの Set)
- #77052 container/mapset(組み込み map を集合として扱うヘルパー)