Goではマップはデフォルトでスレッドセーフではありません。複数のゴルーチンが、同期なしで同じマップを同時に読み書きすると、データ競合が発生し、fatal error: concurrent map read and map writeというパニックやデータの破損が引き起こされます。
マップをスレッドセーフに操作するためには、以下が必要です:
sync.Mutexまたはsync.RWMutexを使用すること。sync.Mapパッケージを使用すること。var mu sync.RWMutex m := make(map[string]int) // 書き込み mu.Lock() m["key"] = 1 mu.Unlock() // 読み取り mu.RLock() v := m["key"] mu.RUnlock() // またはsync.Map var sm sync.Map sm.Store("key", 1) v, _ := sm.Load("key")
なぜGoのマップで異なるゴルーチンから同時に読み書きを行うことがそんなに危険なのか、マップは組み込み型なのに?
回答:Goの組み込み型であるマップは同期を提供しません。複数のゴルーチンからマップに同時にアクセスすると、単に不正確な値が返るだけでなく、プログラムがクラッシュする可能性があります。たとえ交差するキーがない場合でも、同時に読み書きすると致命的なエラーが発生することがあります。これは、他のいくつかの言語のコレクションが競合するアクセスに「耐性」を持つのとは異なります。
事例
実際のプロジェクトで、開発者はデータキャッシングのためにグローバルマップを使用しました。サービスはテストでは安定して動作しましたが、負荷テストや本番環境でfatal error: concurrent map read and map writeのエラーでクラッシュし始めました。原因は、Mutexを使用せずに異なるHTTPリクエストからマップに並行アクセスしていたことです。
事例
Goのウェブアプリケーションで、一人のプログラマーがパフォーマンスを向上させるために通常のマップを接続プールとして使用し、マルチスレッドはフレームワークによって保証されていると考えました。トラフィックが急増すると、サービスは明確な理由もなくクラッシュし始めました:競合によりマップ内のデータが破損し、パニックが発生しました。
事例
社内のGoサービスでは、アプリケーションが「リアルタイム」で統計を収集するためにマップを使用し、主なアクセスは書き込みのみであると考えていました。実際には、コードの別の部分が定期的にデータを要求し、レポートを生成していたため、統計が作動するタイミングで難解なクラッシュが発生しました。分析の結果、読み書きがロックなしで重なっていることが分かりました。