Ответ
Выбор между 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
— это специализированная структура, оптимизированная для двух конкретных случаев:
- Когда ключ записывается один раз, а затем читается много раз (write-once, read-many).
- Когда несколько горутин конкурентно читают, пишут и удаляют записи для разных наборов ключей.
Она работает без глобальной блокировки для большинства операций, используя внутренние механизмы (две мапы: read
и dirty
).
Когда использовать:
- Высокая конкуренция: Множество горутин одновременно читают и пишут данные по разным ключам.
- Ключи стабильны: Набор ключей, к которым идет обращение, со временем меняется мало.
sync.Map
хорошо кэширует такие доступы. - Не нужна строгая типизация: Готовы работать с
interface{}
и делать приведение типов.
Недостатки:
- Медленнее
map
+RWMutex
в сценариях с редкими записями (read-mostly) из-за дополнительной логики. - Неудобный API: Все ключи и значения имеют тип
interface{}
, что требует постоянных приведений типов. - Нет простых операций: Нельзя легко получить размер (
len
) или итерироваться по мапе. Для итерации используется методRange
, который принимает функцию.
Итог и сравнение
Характеристика | map + sync.RWMutex | sync.Map |
---|---|---|
Лучший сценарий | Чтений намного больше, чем записей (Read-Mostly) | Высокая конкуренция, много записей по разным ключам |
Производительность | Очень высокая при низкой конкуренции за запись | Высокая при высокой конкуренции, но медленнее в других случаях |
Безопасность типов | Да (compile-time) | Нет (runtime, через interface{} ) |
Удобство API | Стандартный map API (m[key] , len , range ) | Специализированные методы (Load , Store , Delete , Range ) |
Контроль блокировок | Полный и явный | Скрыт внутри реализации |
Вывод: Начинайте с map
и sync.RWMutex
— это простое, понятное и производительное решение для большинства задач. Переходите на sync.Map
только после профилирования, если вы обнаружили, что именно конкуренция за мьютекс стала узким местом в вашем приложении.