Что такое `sync.Map`? В каких случаях её следует использовать, а когда лучше выбрать `map` с `Mutex`?

Ответ

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

Внутреннее устройство (упрощенно):

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

  • read: Атомарный указатель на read-only карту, доступ к которой для чтения не требует блокировок.
  • dirty: Обычная map с мьютексом для новых записей и обновлений. Периодически dirty карта "продвигается" до read.

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

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

Когда sync.Map — не лучший выбор (и лучше map + sync.RWMutex):

  1. Частые записи и обновления: Если горутины часто перезаписывают значения по одним и тем же ключам, производительность sync.Map может быть ниже из-за накладных расходов на управление dirty картой.
  2. Нужна строгая типизация: sync.Map хранит ключи и значения как interface{}, что требует приведения типов и лишает статической проверки. С появлением дженериков в Go 1.18 стало проще реализовать собственную типизированную конкурентную карту.
  3. Нужно знать размер (len): У sync.Map нет метода Len(), так как его вычисление было бы медленным и требовало бы блокировки.
  4. Статические данные: Если карта инициализируется один раз и больше никогда не изменяется, обычная map без всяких блокировок будет самым быстрым решением.

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

var m sync.Map

// Запись (Store)
m.Store("user:1", "Alice")

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

// Итерация (Range)
m.Range(func(key, value interface{}) bool {
    fmt.Printf("%s -> %sn", key, value)
    return true // для продолжения итерации
})

Ответ 18+ 🔞

Давай разберём эту штуку, которая называется sync.Map. Ну, типа, карта, но с приставкой sync, что намекает на какую-то синхронизацию. Ага, так и есть — это встроенная в Go потокобезопасная мапа, но, блядь, не для всех случаев жизни. Она как спецназ: крута в очень узких операциях, а в остальном — обычный солдат с мьютексом может оказаться лучше.

Что у неё внутри, ёпта? (Грубо говоря)

Представь, у неё две квартиры, простите, карты:

  • read: Карта только для чтения. На неё можно смотреть всем подряд без очереди и без драки. Указатель на неё атомарный.
  • dirty: Грязная, блядь, карта. Вот тут уже бардак, мьютекс и все дела. Туда пишут всё новое. Потом, когда она наберётся, её "повышают" до статуса read.

Когда её впихивать в проект? (Когда она реально охуенна)

  1. Записал один раз — читай до посинения: Идеальный сценарий — кэш каких-нибудь конфигов, которые обновляются раз в год. Записали, а потом 10 тысяч горутин их читают. Тут она блестит, как ядрёна вошь.
  2. Разные ключи — разные горутины: Если одна горутина ковыряется с ключами A, B, C, а другая — с X, Y, Z, и они редко пересекаются. Конфликтов минимум, жизнь хороша.

А когда это пиздопроебибна идея? (Лучше взять обычную map и sync.RWMutex)

  1. Частые перезаписи: Если ты как сумасшедший постоянно обновляешь значения по одним и тем же ключам, то вся эта магия с двумя картами превратится в адские накладные расходы. Обычный мьютекс будет проще и быстрее.
  2. Хочется типов, а не interface{}: sync.Map хранит всё как interface{}, значит, каждый раз при чтении надо делать приведение типа val.(string). Статической проверки — ноль. С дженериками в Go 1.18 можно накодить свою типизированную карту, и будет красота.
  3. Надо узнать размер: Захочешь узнать, сколько там элементов — m.Len(), а его, блядь, нет! Потому что посчитать длину — это заблокировать всё и пересчитать, а они не хотят.
  4. Данные никогда не меняются: Если карта инициализируется при старте и всё — на этом её история заканчивается, то бери обычную map и не парься. Это будет самый быстрый вариант, волнение ебать.

Ну и примерчик, чтобы не быть голословным:

var m sync.Map

// Записали (Store)
m.Store("user:1", "Alice")

// Прочитали (Load)
val, ok := m.Load("user:1")
if ok {
    fmt.Println(val.(string)) // Вот это вот приведение типа — обязательный ритуал
}

// Пробежаться по всем (Range)
m.Range(func(key, value interface{}) bool {
    fmt.Printf("%s -> %sn", key, value)
    return true // вернёшь false — итерация остановится
})

Короче, sync.Map — инструмент не универсальный. Подозрение ебать чувствую, что её часто используют просто потому, что она есть в стандартной библиотеке и звучит умно. Но прежде чем её впилить, подумай: а твой случай — это "записал-читай" или "все пишут и читают в одну точку"? От этого зависит, будешь ты героем или тем самым чуваком, который оптимизировал не там, где надо.