Сравните подходы к конкурентной работе с map в Go: `map` с мьютексом (`sync.Mutex`/`RWMutex`) и `sync.Map`. В каких сценариях каждый из них предпочтительнее?

Ответ

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

1. map + sync.RWMutex

Это классический и наиболее распространенный подход. sync.RWMutex (Read-Write Mutex) позволяет неограниченному числу горутин одновременно читать данные, но запись требует эксклюзивной блокировки, которая блокирует всех остальных (и читателей, и писателей).

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

  • Сценарий "прочитай много, запиши мало" (Read-Mostly): Идеально, когда у вас есть кэш, который инициализируется один раз, а затем в основном только читается. Множественные одновременные чтения не будут блокировать друг друга.
  • Низкая конкуренция за запись: Если операции записи происходят редко и не одновременно, то блокировка не станет узким местом.
  • Нужна типизация: Вы работаете с конкретными типами (map[string]int), что обеспечивает безопасность типов на этапе компиляции и избавляет от необходимости приведения типов (type assertion).
  • Требуется полный контроль: Вам нужны такие операции, как получение длины мапы (len(m)), итерация по ней (for k, v := range m) или удаление ключей, что легко сделать под блокировкой.

Пример:

var (
    cache = make(map[string]string)
    mu    sync.RWMutex
)

// Get получает значение из кэша
func Get(key string) (string, bool) {
    mu.RLock() // Блокировка на чтение
    defer mu.RUnlock()
    val, ok := cache[key]
    return val, ok
}

// Set устанавливает значение в кэш
func Set(key string, value string) {
    mu.Lock() // Эксклюзивная блокировка на запись
    defer mu.Unlock()
    cache[key] = value
}

Недостаток: При высокой конкуренции за запись Mutex становится "бутылочным горлышком", так как каждая запись останавливает все остальные операции.

2. sync.Map

sync.Map — это специализированная структура, оптимизированная для двух конкретных случаев:

  1. Когда ключ записывается один раз, а затем читается много раз (write-once, read-many).
  2. Когда несколько горутин конкурентно читают, пишут и удаляют записи для разных наборов ключей.

Она работает без глобальной блокировки для большинства операций, используя внутренние механизмы (две мапы: read и dirty).

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

  • Высокая конкуренция: Множество горутин одновременно читают и пишут данные по разным ключам.
  • Ключи стабильны: Набор ключей, к которым идет обращение, со временем меняется мало. sync.Map хорошо кэширует такие доступы.
  • Не нужна строгая типизация: Готовы работать с interface{} и делать приведение типов.

Недостатки:

  • Медленнее map + RWMutex в сценариях с редкими записями (read-mostly) из-за дополнительной логики.
  • Неудобный API: Все ключи и значения имеют тип interface{}, что требует постоянных приведений типов.
  • Нет простых операций: Нельзя легко получить размер (len) или итерироваться по мапе. Для итерации используется метод Range, который принимает функцию.

Итог и сравнение

Характеристикаmap + sync.RWMutexsync.Map
Лучший сценарийЧтений намного больше, чем записей (Read-Mostly)Высокая конкуренция, много записей по разным ключам
ПроизводительностьОчень высокая при низкой конкуренции за записьВысокая при высокой конкуренции, но медленнее в других случаях
Безопасность типовДа (compile-time)Нет (runtime, через interface{})
Удобство APIСтандартный map API (m[key], len, range)Специализированные методы (Load, Store, Delete, Range)
Контроль блокировокПолный и явныйСкрыт внутри реализации

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