Go のマップの注意点と落とし穴

Go のマップにはいくつかの落とし穴がある。これらを知っておくことで、バグを未然に防げる。

nil マップへの書き込み

var で宣言しただけのマップは nil であり、書き込むと panic になる。

var m map[string]int
m["key"] = 1 // panic: assignment to entry in nil map

必ず make かリテラルで初期化してから使う。

m := make(map[string]int)
m["key"] = 1 // OK

構造体フィールドの直接更新はできない

マップの値が構造体の場合、フィールドを直接更新できない。

type Person struct {
    Name string
    Age  int
}

people := map[string]Person{
    "alice": {Name: "Alice", Age: 30},
}

people["alice"].Age = 31 // コンパイルエラー

これはマップの値がコピーとして返されるためだ。解決策は2つある。

一度取り出して代入し直す

値を変数に取り出し、変更してから再代入する。

ポインタを値にする

マップの値をポインタにすれば、直接フィールドを更新できる。

// 方法1: 取り出して再代入
p := people["alice"]
p.Age = 31
people["alice"] = p

// 方法2: ポインタを使う
people := map[string]*Person{
    "alice": {Name: "Alice", Age: 30},
}
people["alice"].Age = 31 // OK

並行アクセスは安全ではない

複数のゴルーチンから同時にマップを読み書きすると、データ競合が発生する。

m := make(map[int]int)

// 危険なコード
go func() {
    for i := 0; i < 1000; i++ {
        m[i] = i
    }
}()

go func() {
    for i := 0; i < 1000; i++ {
        _ = m[i]
    }
}()
// fatal error: concurrent map read and map write

並行アクセスが必要な場合は sync.Mutex か sync.RWMutex で保護する。

var mu sync.RWMutex
m := make(map[int]int)

// 書き込み
mu.Lock()
m[1] = 100
mu.Unlock()

// 読み込み
mu.RLock()
v := m[1]
mu.RUnlock()

マップの比較はできない

マップ同士を == で比較することはできない。

m1 := map[string]int{"a": 1}
m2 := map[string]int{"a": 1}

fmt.Println(m1 == m2) // コンパイルエラー

nil との比較だけは可能だ。

var m map[string]int
fmt.Println(m == nil) // true

マップの内容を比較したい場合は、reflect.DeepEqual を使うか、自分でループして比較する。

import "reflect"

m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"a": 1, "b": 2}

fmt.Println(reflect.DeepEqual(m1, m2)) // true

キーにできない型

スライス、マップ、関数はキーにできない。

// コンパイルエラー
m := map[[]int]string{}      // スライスはNG
m := map[map[int]int]string{} // マップはNG
m := map[func()]string{}      // 関数はNG

配列はキーにできる。

m := map[[3]int]string{
    {1, 2, 3}: "one-two-three",
}

ゼロ値の罠

存在しないキーにアクセスするとゼロ値が返るため、意図しない動作になることがある。

counts := make(map[string]int)

// 存在しないキーでもエラーにならない
counts["apple"]++ // 0 + 1 = 1
counts["apple"]++ // 1 + 1 = 2

fmt.Println(counts["apple"]) // 2

この挙動はカウンターとして使う場合には便利だが、存在チェックが必要な場面ではコンマOK イディオムを使うこと。