Потокобезопасная мапа: sync.Map
Обычная map в Go не является потокобезопасной. Если одна горутина пишет в мапу, а другая одновременно читает или пишет, программа упадет с ошибкой fatal error: concurrent map writes (отловить это через recover невозможно, приложение просто умрет).
Самое простое решение — обернуть обычную map в структуру с мьютексом (sync.RWMutex). Но в стандартной библиотеке есть специальный тип — sync.Map.
1. Зачем нужен sync.Map, если есть RWMutex?
Проблема мьютекса в том, что при огромном количестве одновременных чтений на многоядерных процессорах возникает lock contention (борьба за блокировку). Даже RLock() (блокировка на чтение) заставляет ядра процессора синхронизировать кэши между собой, что снижает производительность.
sync.Map спроектирована так, чтобы читать данные без блокировок вообще (lock-free), используя атомарные операции (atomic).
2. Как sync.Map работает под капотом?
Внутри sync.Map использует не одну, а две обычные мапы:
read(мапа для чтения): Доступ к ней происходит атомарно, без мьютексов. Она предназначена только для чтения и обновления уже существующих значений.dirty(грязная мапа): Обычная мапа, доступ к которой защищен встроенным мьютексом. В нее записываются новые ключи.
Алгоритм работы:
- Чтение (
Load): Сначала Go ищет ключ вreadмапе (без блокировок). Если ключа нет, но он может быть вdirty, берется блокировка, и ключ ищется вdirty. - Запись (
Store): Если ключ уже есть вread, значение обновляется атомарно. Если это новый ключ — берется блокировка и он пишется вdirty. - Промоушен (Promotion): Если чтений из
dirtyмапы становится слишком много (счетчик промаховmissesпревышает порог),dirtyмапа атомарно "повышается" и становится новойreadмапой.
3. Когда использовать sync.Map?
У sync.Map есть только два идеальных юзкейса (это прямо написано в официальной документации Go):
- Кэширование (Write-once, Read-many): Когда ключи добавляются один раз, а потом бесконечно читаются многими горутинами.
- Раздельные пространства ключей: Когда разные горутины работают с совершенно разными непересекающимися ключами в одной мапе.
Интервью Если на собеседовании спросят: "Что лучше использовать?", правильный ответ: "В 90% случаев обычная
map+sync.RWMutexбудет быстрее и понятнее.sync.Mapстоит брать только если профилирование (pprof) показало, что мьютекс стал узким горлышком из-за огромного количества чтений."
4. Основные методы sync.Map
Вместо обычного синтаксиса m[key], здесь используются методы:
Store(key, value)— записать.Load(key) (value, ok)— прочитать.Delete(key)— удалить.LoadOrStore(key, value)— атомарно возвращает значение, если оно есть. Если нет — записывает новое.Range(func(key, value any) bool)— итерация по мапе (если функция вернетfalse, цикл прервется).