Что такое `sync.Map` в Go? В каких случаях его стоит использовать, а когда лучше подойдет `map` с `sync.RWMutex`?

Ответ

sync.Map — это встроенный в Go тип данных, представляющий собой конкурентно-безопасную хэш-таблицу, оптимизированную для очень специфичных сценариев использования.

Как он устроен (упрощенно)?

sync.Map внутренне состоит из двух хэш-таблиц:

  1. read — основная таблица, доступная для чтения без блокировок.
  2. dirty — таблица для новых записей, доступ к которой требует блокировки.
  • Чтение (Load): Сначала ключ ищется в read. Если найден — возвращается результат (быстро, без блокировки). Если нет — ставится блокировка и ключ ищется в dirty.
  • Запись (Store): Всегда требует блокировки и происходит в dirty.
  • Периодически, если dirty содержит ключи, которых нет в read, dirty «продвигается» и становится новой read-таблицей.

Эта архитектура объясняет, почему sync.Map быстр на чтение, но медленнее на запись.

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

Его стоит применять только в двух случаях:

  1. Сценарий "write-once, read-many": Когда ключ записывается один раз, а затем многократно читается множеством горутин. Например, кэш редко изменяемых данных.
  2. Раздельные наборы ключей: Когда множество горутин работает с картой, но каждая горутина обращается к своему собственному, непересекающемуся с другими набору ключей. Это минимизирует конкуренцию за dirty.

Когда использовать map + sync.RWMutex?

Это предпочтительный вариант в большинстве случаев:

  • Когда количество чтений и записей примерно сопоставимо.
  • Когда требуется строгая типизация (например, map[string]int), так как sync.Map работает только с interface{}.
  • Когда карта инициализируется один раз, а затем передается в горутины только для чтения (становится неизменяемой).
  • Когда производительность не является узким местом, а простота и читаемость кода важнее.

Пример использования sync.Map:

var m sync.Map

// Запись значения
m.Store("user:1", "Alice")

// Чтение значения
value, ok := m.Load("user:1")
if ok {
    fmt.Println(value.(string)) // Требуется приведение типа
}

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

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