Menu

Go言語のmap:作成、キーの存在確認、ループ、ソート、削除

Goのマップはキーと値のペアを保存し、高速に検索できます。作り方、カンマokでキーの存在を確認する方法、削除、(ランダムな順序での)走査、キーのソート、構造体の格納、そしてnilマップと並行書き込みによるpanicの避け方を解説します。

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

マップを作って使う

マップの型は map[KeyType]ValueType と書きます。リテラルか make で作り、キーで読み、書き、削除します。

出力:

31
3
map[bob:26 cy:40]
2 0

ここには便利な点が2つ表れています。存在しないキーを読むと値の型のゼロ値が返る(counts['z']0)ので、準備なしに m[k]++ で数えられます。そして fmt はキーをソートしてマップを表示します。デバッグには便利ですが、反復順については何も語りません。

make(map[K]V, n) は省略可能なサイズのヒントを受け取ります。約 n 個のエントリのための領域を事前に確保しますが、スライスと違って、マップには読み戻せる容量はありません。

キーの存在を確認する:カンマok

存在しないキーはゼロ値として読めるので、m[k] == 0 では「ない」と「0として保存されている」を区別できません。2値の形を使います。

if v, ok := m[k]; ok { ... } の形にすると、vok のスコープが if の中に収まります。Goのコードで最もよく見る行の1つです。

エントリを削除する

delete(m, key) はエントリを取り除きます。存在しないキーの削除は何もせず、nilマップからの削除も同じです。マップ全体を空にするために、Go 1.21で clear(m) が追加されました。確保済みのマップを残すので、再利用できます。

同じマップを range している間にエントリを削除するのは許されていて安全です。ループが到達する前に削除されたエントリは返されません。

走査する:順序はランダム

for k, v := range m はすべてのエントリを規定されていない順序で1回ずつ訪れます。ランタイムが開始位置を意図的にランダムにするので、同じプログラム内の同じマップに対する2つのループでも順序が食い違うことがよくあります。何度か実行してみてください。

マップの順序に出力が依存するコードは、別の実行で表に出るのを待っているバグです。マップの反復結果を表示して比較するテストが典型例です。

キーをソートする

キーの順にマップを訪れるには、キーを取り出してソートし、マップにインデックスでアクセスします。Go 1.23では mapsslices パッケージのイテレータで1行になりました。

Go 1.22以前では、maps.Keys は標準ライブラリにありませんでした。同じことをするにはループを書きます。

keys := make([]string, 0, len(m))
for k := range m {
	keys = append(keys, k)
}
sort.Strings(keys)

maps の他のヘルパー:maps.Valuesmaps.Clone(浅いコピー)、maps.Equalmaps.Copy(dst, src)maps.DeleteFunc

キーに使える型

キーは == で比較可能でなければなりません。数値、文字列、真偽値、ポインタ、チャネル、比較可能な型の配列、すべてのフィールドが比較可能な構造体、インターフェースの値です。スライス、マップ、関数はキーになれません。

map[[]int]bool{}       // compile error: invalid map key type []int
map[[2]int]bool{}      // fine: arrays are comparable
map[struct{ X, Y int }]string{} // fine: a struct key for a grid position

複数の値をまとめてキーにするには、文字列を連結するのではなく構造体のキーを使うのがGoらしいやり方です。

インターフェースのキーは、動的な型が比較可能でなくてもコンパイルでき、実行時にpanicします。map[any]int[]int をキーとして格納すると、runtime error: hash of unhashable type []int で失敗します。

浮動小数点数のキーも使えますが、NaN は自分自身と等しくないので、NaN のキーは何度でも挿入でき、決して読み戻せません。浮動小数点数のキーは避けましょう。

構造体のマップ

マップは構造体を保持できますが、マップの値はアドレスを取れないので、マップに格納された構造体のフィールドに代入することはできません。

エントリが小さく、まるごと置き換えるなら値を選びます。フィールドを頻繁に更新したり、同じレコードを複数の場所で共有したりするならポインタを選びます。ポインタの場合、存在しないキーは nil を返すので、ptrs["nope"].Score はpanicします。

スライスのマップへのappendも同じように動きます。groups[k] = append(groups[k], v) は初期化が要りません。存在しないキーはnilスライスを返し、append はnilを扱えるからです。

マップは参照のように振る舞う

マップの値は共有データを参照しています。マップを代入したり関数に渡したりしてもエントリはコピーされず、どちらの変数も同じマップを見ます。

appendするスライスと違って、関数が返さなくてもマップを埋められるのはこのためです。

nilマップのpanic

マップのゼロ値は nil です。nilマップは空のマップのように読めますが、書き込むとpanicします。

出力:

0 0
recovered: assignment to entry in nil map

実際に問題になるのは構造体のケースです。マップのフィールドはコンストラクタで初期化する(func NewCache() *Cache { return &Cache{data: map[string]string{}} })か、最初の書き込みの前に遅延初期化します。

並行アクセス

マップは並行処理で安全に使えません。あるゴルーチンが書き込んでいる間に別のゴルーチンが読み書きすると、ランタイムが fatal error: concurrent map writes(または concurrent map read and map write)でプログラムを止めることがあります。これはpanicではなく致命的エラーなので、recover でも捕まえられません。

ミューテックスでマップを保護します。

これは常に 50 50 と表示します。読み取りが書き込みよりはるかに多いなら sync.RWMutex を使います。sync.Map が存在するのは2つの限られたケース(一度書かれて何度も読まれるキー、またはゴルーチンが互いに重ならないキーを扱う場合)のためで、それ以外はミューテックスと普通のマップのほうが単純で、たいていは高速です。詳しくはミューテックスのページを参照してください。

早見表

操作コード
作成m := map[string]int{} または make(map[string]int)
挿入または更新m[k] = v
読み取り(なければゼロ値)v := m[k]
存在確認v, ok := m[k]
削除delete(m, k)
すべて削除clear(m)(Go 1.21)
サイズlen(m)
ソート済みのキーslices.Sorted(maps.Keys(m))(Go 1.23)
コピーmaps.Clone(m)
比較maps.Equal(a, b)

値が struct{} のマップはGoの集合型でもあります。集合のページを参照してください。

よくある間違い

  • nilマップに書き込む。 構造体のマップのフィールドも含め、必ず make します。
  • 反復順に頼る。 キーをソートします。
  • 存在確認に m[k] != 0 を使う。 カンマokを使います。
  • m[k].Field で構造体のフィールドを変更する。 取り出して書き戻すか、ポインタを格納します。
  • ロックなしでゴルーチン間でマップを共有する。 そのクラッシュは回復できません。

よくある質問

Goのマップにキーが存在するか確認するには?

参照の2値の形を使います:v, ok := m[key]。キーがあれば oktrue、なければ false で、そのとき v はゼロ値です。m[key] を読むだけでは、キーがないのか、ゼロ値が保存されているのかを区別できません。

なぜGoのマップの反復順はランダムなのですか?

言語が順序を定めておらず、プログラムが特定の順序に依存しないよう、ランタイムが意図的に各 range をランダムな位置から始めるからです。キーの順に走査するには、キーを集めてソートします:for _, k := range slices.Sorted(maps.Keys(m))(Go 1.23)。

Goでマップのキーをすべて取得するには?

Go 1.23以降、maps.Keys(m) はイテレータを返します。slices.Collect(maps.Keys(m)) でスライスに、slices.Sorted(maps.Keys(m)) でソート済みのスライスにできます。1.23より前は、for k := range m でループして各キーをスライスに追加します。

なぜマップへの書き込みが「assignment to entry in nil map」でpanicするのですか?

マップの変数を宣言しただけで作成していないからです:var m map[string]int はnilです。nilマップからの読み取りはゼロ値を返しますが、書き込むとpanicします。先に m = make(map[string]int) かリテラル m := map[string]int{} で作ります。構造体の中のマップのフィールドも同じ初期化が必要です。

Goのマップは並行処理で安全に使えますか?

使えません。並行した書き込み、または読み取りと並行した書き込みは、fatal error: concurrent map writes でプログラムをクラッシュさせることがあり、recover でも捕まえられません。sync.Mutexsync.RWMutex でマップを保護するか、sync.Map が想定している特定のケースではそれを使います。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める