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 イディオムを使うこと。