Как синхронизировать Map в Go?

Ответ

В Go для потокобезопасной работы с map в конкурентной среде можно использовать несколько подходов. Наиболее распространенные:

  1. sync.Map (специализированный случай): sync.Map предоставляет потокобезопасную реализацию map, оптимизированную для случаев, когда записи происходят редко, а чтения — часто, или когда несколько горутин записывают уникальные ключи.

    Основные методы:

    • Store(key, value) — сохраняет пару ключ-значение.
    • Load(key) — возвращает значение по ключу и булевый флаг наличия (value, ok).
    • LoadOrStore(key, value) — загружает значение по ключу, если оно существует; иначе сохраняет новое значение. Возвращает фактическое значение и булевый флаг, указывающий, было ли значение загружено (actual, loaded).
    • Delete(key) — удаляет ключ.
    • Range(func(key, value) bool) — итерирует по map. Функция-колбэк должна возвращать true для продолжения итерации, false для остановки.

    Пример:

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    func main() {
        var m sync.Map
    
        m.Store("key1", "value1")
        val, ok := m.Load("key1")
        if ok {
            fmt.Printf("Загружено: %vn", val) // Загружено: value1
        }
    
        actual, loaded := m.LoadOrStore("key1", "newValue")
        fmt.Printf("LoadOrStore (существующий): actual=%v, loaded=%tn", actual, loaded) // actual=value1, loaded=true
    
        actual, loaded = m.LoadOrStore("key2", "value2")
        fmt.Printf("LoadOrStore (новый): actual=%v, loaded=%tn", actual, loaded) // actual=value2, loaded=false
    
        m.Range(func(k, v interface{}) bool {
            fmt.Printf("Ключ: %v, Значение: %vn", k, v)
            return true
        })
    
        m.Delete("key1")
        _, ok = m.Load("key1")
        fmt.Printf("Key1 существует после удаления: %tn", ok) // Key1 существует после удаления: false
    }

    Когда использовать sync.Map:

    • Когда есть много горутин, читающих данные, и относительно мало горутин, записывающих данные.
    • Когда набор ключей стабилен, но значения могут обновляться.
    • Когда ключи добавляются только один раз (например, кэш).
    • Избегайте sync.Map для частых обновлений или когда большинство операций — это записи/удаления, так как в этих случаях она может быть медленнее, чем обычная map с мьютексом.
  2. sync.Mutex или sync.RWMutex с обычной map (наиболее общий случай): Это наиболее гибкий и часто используемый подход для синхронизации map.

    • sync.Mutex: Простая блокировка, которая позволяет только одной горутине одновременно читать или записывать в map.
    • sync.RWMutex: Блокировка для чтения/записи. Позволяет множеству горутин читать одновременно, но только одной горутине записывать (и блокирует все чтения во время записи).

    Пример с sync.RWMutex:

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    type SafeMap struct {
        m map[string]string
        mu sync.RWMutex
    }
    
    func NewSafeMap() *SafeMap {
        return &SafeMap{m: make(map[string]string)}
    }
    
    func (sm *SafeMap) Store(key, value string) {
        sm.mu.Lock() // Блокируем для записи
        defer sm.mu.Unlock()
        sm.m[key] = value
    }
    
    func (sm *SafeMap) Load(key string) (string, bool) {
        sm.mu.RLock() // Блокируем для чтения
        defer sm.mu.RUnlock()
        val, ok := sm.m[key]
        return val, ok
    }
    
    func main() {
        sm := NewSafeMap()
    
        sm.Store("city", "New York")
        val, ok := sm.Load("city")
        if ok {
            fmt.Printf("Город: %sn", val)
        }
    }

    Когда использовать sync.Mutex / sync.RWMutex:

    • Это подход по умолчанию, когда требуется потокобезопасная map.
    • RWMutex предпочтителен, если чтений значительно больше, чем записей.
    • Mutex проще, если операции чтения и записи примерно равны по частоте или если операции очень короткие.

Выбор между sync.Map и sync.Mutex/sync.RWMutex зависит от конкретного паттерна доступа к данным в вашем приложении.