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 には単純な読み書き以外にも便利なメソッドがあります。

LoadOrStore

キーが存在すれば既存の値を返し、なければ新しい値を格納して返す。初期化処理に便利。

LoadAndDelete

値を取得すると同時に削除する。一度だけ取り出したい場合に使える。

Range

すべてのキーと値を順番に処理する。ただし、処理中に他の 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 は万能ではありません。特定のユースケースで最適化されています。

sync.Map が適している場面

キーが一度書き込まれたらほぼ変更されない場合。多くの goroutine が同時に異なるキーを読み書きする場合。

map + Mutex が適している場面

頻繁に同じキーを更新する場合。要素数を取得したい場合(sync.Map には Len がない)。型安全性が必要な場合。

sync.Map は内部でキャッシュを使い、読み取りが多く書き込みが少ないワークロードで高いパフォーマンスを発揮します。逆に、書き込みが頻繁な場合は mapsync.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 以降ではジェネリクスを使った独自のラッパーを作ることで、型安全な並行マップを実装することも可能です。用途に応じて使い分けましょう。