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 の組み合わせのほうがパフォーマンスが良いことが多い。用途に応じて選択しよう。