Go の sync.Map で並行安全なマップを使う
通常の map は並行アクセスに対応していない。複数のゴルーチンから安全にアクセスしたい場合、sync.Map を使う方法がある。
sync.Map とは
sync パッケージが提供する並行安全なマップだ。ロックなしで安全に読み書きできる。
import "sync"
var m sync.Map通常のマップと違い、宣言するだけで使える。make は不要だ。
基本操作
通常のマップとはメソッドが異なる。
var m sync.Map
// 保存
m.Store("name", "Alice")
m.Store("age", 30)
// 取得
value, ok := m.Load("name")
if ok {
fmt.Println(value) // Alice
}
// 削除
m.Delete("age")LoadOrStore
キーが存在しなければ保存し、存在すれば既存の値を返す。
var m sync.Map
// 初回は保存される
actual, loaded := m.LoadOrStore("key", "first")
fmt.Println(actual, loaded) // first false
// 2回目は既存の値が返る
actual, loaded = m.LoadOrStore("key", "second")
fmt.Println(actual, loaded) // first trueキャッシュの初期化などで便利だ。
LoadAndDelete
値を取得すると同時に削除する。
var m sync.Map
m.Store("token", "abc123")
value, loaded := m.LoadAndDelete("token")
fmt.Println(value, loaded) // abc123 true
// 再度呼ぶと存在しない
value, loaded = m.LoadAndDelete("token")
fmt.Println(value, loaded) // <nil> falseワンタイムトークンの処理などに使える。
Range でイテレーション
Range メソッドで全要素を処理する。
var m sync.Map
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 を返すとループ終了
})型安全性がない
sync.Map は interface{} でキーと値を扱うため、型安全ではない。
var m sync.Map
m.Store("count", 100)
value, _ := m.Load("count")
count := value.(int) // 型アサーションが必要型安全にしたい場合は、ラッパーを作る。
type SafeCounter struct {
m sync.Map
}
func (c *SafeCounter) Inc(key string) {
v, _ := c.m.LoadOrStore(key, new(int64))
atomic.AddInt64(v.(*int64), 1)
}
func (c *SafeCounter) Get(key string) int64 {
v, ok := c.m.Load(key)
if !ok {
return 0
}
return atomic.LoadInt64(v.(*int64))
}sync.Map を使うべきケース
sync.Map が向いているケース
キーが安定していて、読み取りが多い場合。一度書き込んだら更新が少ないキャッシュなど。
通常の map + Mutex が向いているケース
頻繁に更新がある場合や、型安全性が重要な場合。
sync.Map のドキュメントには、以下の2つのケースで最適化されていると記載されている。
キーが一度だけ書き込まれる
エントリが一度だけ書き込まれ、その後は読み取りのみの場合。キャッシュなど。
複数のゴルーチンが独立したキーを使う
ゴルーチンごとに異なるキーにアクセスする場合。競合が少ない。
汎用的な並行マップが必要な場合は、通常の map と sync.RWMutex の組み合わせのほうがパフォーマンスが良いことが多い。用途に応じて選択しよう。