Menu

Go言語のSet(集合):map[T]struct{}、集合演算、ジェネリックなSet

Goに組み込みの集合型はありません。標準的なイディオムは、値を空の構造体にしたマップです。追加、存在確認、削除、和集合、積集合、差集合、そして小さなジェネリックなSetの書き方を解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

イディオム:値が空のマップ

Goに set キーワードはなく、標準ライブラリにも集合はありません。要素をキーにして、値に何も持たせないマップがその役目を果たします。

struct{} は空の構造体の型で、struct{}{} はその唯一の値です。0バイトしか使わないので、マップはキーだけを保存します。

マップのルールはすべて当てはまります。キーは比較可能でなければならず、反復順はランダムで、nilマップへの書き込みはpanicし、並行した書き込みにはロックが必要です。それぞれマップのページで扱っています。

map[T]struct{}かmap[T]boolか

もう1つよく使われる書き方は map[T]bool です。存在しないキーが false を返すので、こちらのほうが読みやすくなります。

seen := map[string]bool{}
seen["a"] = true
if seen["a"] { ... }

トレードオフは次のとおりです。

map[T]struct{}map[T]bool
値のサイズ0バイト1バイト(加えてアラインメント)
存在確認_, ok := s[k]s[k]
あいまいさなしs[k] = false が3つ目の状態になる

多くのコードベースが空の構造体を好む本当の理由は3行目です。bool を使うと、いつか誰かが s[k] = false と書き、len(s) がメンバーの数ではなくなります。小さな集合ならメモリの差は問題になりません。

スライスから重複を取り除く

集合の最もよくある用途は重複の除去です。次のコードは各値の最初の出現を残し、順序を保ちます。

出力:

[b a c]
[3 1 2]
[1 2 3]

和集合、積集合、差集合

集合の演算は数個のループで書けます。一方の集合での存在確認をするときは、小さいほうの集合を走査します。各検索は平均して定数時間だからです。

sorted は出力を安定させるためだけにあります。集合をrangeして表示すると、実行するたびに順序が変わります。maps.Keysslices.Sorted にはGo 1.23が必要です。

type set map[string]struct{} のような名前付きの型もマップであることに変わりはありません。同じようにインデックスを使い、range し、delete でき、メソッドを付けることもできます。

小さなジェネリックなSet

ジェネリクス(Go 1.18)を使えば、1つの型で比較可能なすべての要素の型をまかなえます。ほとんどのプログラムにはこれで十分です。

マップを構造体で包むと struct{}{} の雑音が隠れ、マップがコンストラクタで確実に作られるので、nilマップのpanicもなくなります。Sorted はメソッドにできません。メソッドは独自の型パラメータを宣言することも、型の comparable 制約を厳しくすることもできず、ソートには cmp.Ordered が必要だからです。制約についてはジェネリクスのページで説明しています。

多機能な集合(スレッドセーフな版、多数の操作)が必要なら、github.com/deckarep/golang-set のようなサードパーティのパッケージもあります。ほとんどのコードでは、Goプログラマーはマップのイディオムか、このような30行ほどの型を使っています。

構造体の集合

比較可能な型なら何でも要素になれ、比較可能なフィールドを持つ構造体も含まれます。これで「このペアを見たことがあるか」のチェックが直接書けます。

type edge struct{ from, to string }
visited := map[edge]struct{}{}
visited[edge{"a", "b"}] = struct{}{}

スライスとマップは集合の要素になれません。一意なスライスを記録するには、固定サイズの配列や fmt.Sprint で作った文字列など、先にそれぞれを比較可能なキーに変換します。

よくある間違い

  • 初期化を忘れる。 var s map[string]struct{} はnilで、最初の追加でpanicします。
  • 集合を表示して安定した出力を期待する。 先にメンバーをソートします。
  • map[T]bool を使って false を格納する。 すると len がメンバーの数を数えなくなります。削除には delete を使います。

よくある質問

Goに集合(Set)型はありますか?

ありません。標準ライブラリに集合はありません。イディオムは、値に情報を持たないマップ map[string]struct{} です。追加は s[k] = struct{}{}、存在確認は _, ok := s[k]、削除は delete(s, k)、サイズは len(s) です。

Goの集合にはmap[T]boolとmap[T]struct{}のどちらを使うべきですか?

map[T]struct{} は意図が明確で、値は0バイトです。map[T]bool は存在しないキーが false を返すので、if seen[x] のように自然に読めます。どちらも正しく、メモリの差が問題になるのは非常に大きな集合だけです。どちらかを選んで一貫させましょう。

Goでスライスから重複を取り除くには?

最初に現れたものを順序どおりに残すなら、ループしながら見た値を map[T]struct{} で記録し、まだ見ていないものだけを追加します。順序が重要でなければ、ソートして隣り合う重複を取り除きます:slices.Sort(s); s = slices.Compact(s)

Coddy programming languages illustration

Coddyでコードを学ぼう

始める