Goでは、mapはデフォルトでスレッドセーフでないデータ構造です。異なるゴルーチンからmapに同時に書き込みや変更を行うと、レースコンディションや「concurrent map writes」というパニックが発生します。mapを保護するために通常はsync.Mutex、sync.RWMutex、または標準ライブラリの特殊な型sync.Mapを使用して、安全な原子操作を実現します。
mapから要素を削除するにはdelete(map, key)を使用しますが、mapに対してrangeを使ったイテレーション中にはいくつかの注意点があります:
range中にmapに新しい要素を追加することはできますが、新しいキーが現在のイテレーションで処理されることは保証されません。スレッドセーフなmapの使用例:
type SafeMap struct { mu sync.RWMutex m map[string]int } func (s *SafeMap) Load(key string) (int, bool) { s.mu.RLock() v, ok := s.m[key] s.mu.RUnlock() return v, ok } func (s *SafeMap) Store(key string, value int) { s.mu.Lock() s.m[key] = value s.mu.Unlock() }
質問:イテレーション中にmapから要素を安全に削除できますか?
答え: はい、ただし、範囲イテレーションで取得されたキーを削除する場合のみです。しかし、他のゴルーチンからmapを並行して変更してはいけません:それはレースを引き起こします。
m := map[string]int{"a": 1, "b": 2, "c": 3} for k := range m { delete(m, k) // 安全!(このゴルーチンからのみ行う場合) }
物語
ある監視サービスがmapに関する統計を記録しており、複数のゴルーチンから同時に更新されていました(メトリックのカウンター)。ピーク時に「concurrent map writes」によるパニックが発生し、サービスが停止し、データが失われました。解決策:mutexを追加するか、通常のmapの代わりにsync.Mapを使用すること。
物語
データ移行中に、誰かが大きなmapのクリーンアップをスピードアップするために、各ゴルーチンが範囲を使って自分のキーの一部を削除しようとしました。その結果、常にデータレースと予測不可能なクラッシュが発生しました。移行後、逐次的なクリーンアップに戻るか、範囲の時間中にアクセスをブロックする必要がありました。
物語
mapに対するrangeループの中で、開発者は同じmapに新しいデータを追加しました(例えば、グラフの隣接ノードのリストを形成していました)。新しいキーが現在の範囲のイテレーションで無視される可能性があることが判明し、グラフの不完全な処理を引き起こしました。バグは珍しいケースを完全にテストするまで特定されませんでした。修正後、アルゴリズムは追加のキューを使用するように書き直されました。