В каких случаях и для чего используется `sync.Map`?

Ответ

sync.Map — это встроенный в Go тип данных, представляющий собой потокобезопасную (конкурентную) map. Он спроектирован и оптимизирован для конкретных сценариев, а не для общей замены map + sync.RWMutex.

Основные сценарии использования:

  1. Кэш "записал один раз, читай много": Когда ключ записывается в map один раз, а затем многократно читается множеством горутин. Например, кэш метаданных или конфигурации.
  2. Раздельные наборы ключей: Когда разные горутины в основном работают с непересекающимися наборами ключей, что минимизирует конфликты при записи.

Как это работает под капотом?

sync.Map использует две внутренние map: read (только для чтения) и dirty (для записи).

  • Чтение существующих ключей происходит из read без блокировок, что делает его очень быстрым.
  • Запись, обновление или чтение нового ключа требует блокировки и работы с dirty map. Периодически данные из dirty переносятся в read.

Преимущества перед map + sync.RWMutex:

  • Отсутствие глобальной блокировки на чтение для уже существующих ключей, что значительно повышает производительность в read-mostly сценариях.

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

  • Для частых операций записи и удаления, особенно одних и тех же ключей. Это вызовет постоянную блокировку dirty map.
  • Если map инициализируется один раз и больше никогда не изменяется (достаточно обычной map).
  • Если требуется перебирать все элементы map. Метод Range менее удобен и производителен, чем for ... range для обычной map.

Пример:

var m sync.Map

// Запись значения. Потокобезопасно.
m.Store("user:1", "John Doe")

// Чтение значения. Потокобезопасно.
val, ok := m.Load("user:1")
if ok {
    fmt.Println(val) // "John Doe"
}

// Запись и чтение в одной операции
actual, loaded := m.LoadOrStore("user:2", "Jane Doe")

// Удаление
m.Delete("user:1")