Go の sync.Map で並行安全なマップを使う
sync.Map は、複数の goroutine から安全にアクセスできる並行安全なマップです。通常の map は並行アクセスに対応していないため、複数の goroutine から読み書きするとデータ競合が発生します。sync.Map はそれを解決します。
通常の map の問題
通常の map を複数の goroutine から操作すると、プログラムがクラッシュする可能性があります。
// これは危険!
m := make(map[string]int)
go func() {
for i := 0; i < 1000; i++ {
m["key"] = i
}
}()
go func() {
for i := 0; i < 1000; i++ {
_ = m["key"]
}
}()このコードを実行すると fatal error: concurrent map read and map write が発生することがあります。
sync.Map の基本操作
sync.Map は Store で格納、Load で取得、Delete で削除を行います。
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// 格納
m.Store("name", "Alice")
m.Store("age", 30)
// 取得
if v, ok := m.Load("name"); ok {
fmt.Println("name:", v)
}
// 削除
m.Delete("age")
// 存在確認
if _, ok := m.Load("age"); !ok {
fmt.Println("age は削除されました")
}
}Load は値と存在フラグの2つを返します。キーが存在しない場合、値は nil になり、フラグは false になります。
便利なメソッド
sync.Map には単純な読み書き以外にも便利なメソッドがあります。
キーが存在すれば既存の値を返し、なければ新しい値を格納して返す。初期化処理に便利。
値を取得すると同時に削除する。一度だけ取り出したい場合に使える。
すべてのキーと値を順番に処理する。ただし、処理中に他の goroutine が変更を加える可能性がある点に注意。
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// LoadOrStore: なければ格納、あれば取得
actual, loaded := m.LoadOrStore("counter", 0)
fmt.Printf("値: %v, 既存だったか: %v\n", actual, loaded)
// 2回目は既存の値が返る
actual, loaded = m.LoadOrStore("counter", 100)
fmt.Printf("値: %v, 既存だったか: %v\n", actual, loaded)
// Range: すべての要素を走査
m.Store("a", 1)
m.Store("b", 2)
m.Store("c", 3)
m.Range(func(key, value interface{}) bool {
fmt.Printf("%v: %v\n", key, value)
return true // false を返すと走査を中断
})
}並行アクセスの例
複数の goroutine から安全にカウンターを操作する例です。
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
var wg sync.WaitGroup
// 複数の goroutine から書き込み
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
key := fmt.Sprintf("worker-%d", i)
m.Store(key, i*10)
}(i)
}
wg.Wait()
// 結果を確認
m.Range(func(key, value interface{}) bool {
fmt.Printf("%s: %v\n", key, value)
return true
})
}Mutex でロックを取る必要がなく、シンプルに書けます。
sync.Map を使うべき場面
sync.Map は万能ではありません。特定のユースケースで最適化されています。
キーが一度書き込まれたらほぼ変更されない場合。多くの goroutine が同時に異なるキーを読み書きする場合。
頻繁に同じキーを更新する場合。要素数を取得したい場合(sync.Map には Len がない)。型安全性が必要な場合。
sync.Map は内部でキャッシュを使い、読み取りが多く書き込みが少ないワークロードで高いパフォーマンスを発揮します。逆に、書き込みが頻繁な場合は map と sync.RWMutex の組み合わせのほうが効率的なこともあります。
型アサーションについて
sync.Map の値は interface{} 型で格納されるため、取り出すときに型アサーションが必要です。
var m sync.Map
m.Store("count", 42)
if v, ok := m.Load("count"); ok {
count := v.(int) // 型アサーション
fmt.Println(count + 1)
}Go 1.18 以降ではジェネリクスを使った独自のラッパーを作ることで、型安全な並行マップを実装することも可能です。用途に応じて使い分けましょう。