マップを作って使う
マップの型は 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 { ... } の形にすると、v と ok のスコープが if の中に収まります。Goのコードで最もよく見る行の1つです。
エントリを削除する
delete(m, key) はエントリを取り除きます。存在しないキーの削除は何もせず、nilマップからの削除も同じです。マップ全体を空にするために、Go 1.21で clear(m) が追加されました。確保済みのマップを残すので、再利用できます。
同じマップを range している間にエントリを削除するのは許されていて安全です。ループが到達する前に削除されたエントリは返されません。
走査する:順序はランダム
for k, v := range m はすべてのエントリを規定されていない順序で1回ずつ訪れます。ランタイムが開始位置を意図的にランダムにするので、同じプログラム内の同じマップに対する2つのループでも順序が食い違うことがよくあります。何度か実行してみてください。
マップの順序に出力が依存するコードは、別の実行で表に出るのを待っているバグです。マップの反復結果を表示して比較するテストが典型例です。
キーをソートする
キーの順にマップを訪れるには、キーを取り出してソートし、マップにインデックスでアクセスします。Go 1.23では maps と slices パッケージのイテレータで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.Values、maps.Clone(浅いコピー)、maps.Equal、maps.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]。キーがあれば ok は true、なければ 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.Mutex か sync.RWMutex でマップを保護するか、sync.Map が想定している特定のケースではそれを使います。