Потокобезопасна ли стандартная `map` в Go?

Ответ

Нет, стандартный тип map в Go не является потокобезопасным. Одновременная запись и чтение из map разными горутинами приведет к состоянию гонки (race condition), что может вызвать панику приложения или привести к непредсказуемым результатам.

Для обеспечения потокобезопасности при работе с map используются следующие подходы:

  1. sync.Mutex или sync.RWMutex: Классический способ защиты с помощью мьютекса. RWMutex предпочтительнее, когда количество чтений значительно превышает количество записей, так как он позволяет выполнять множество чтений одновременно.

    Пример с sync.RWMutex

    type SafeMap struct {
        mu sync.RWMutex
        m  map[string]int
    }
    
    func (s *SafeMap) Get(key string) (int, bool) {
        s.mu.RLock() // Блокировка на чтение
        defer s.mu.RUnlock()
        val, ok := s.m[key]
        return val, ok
    }
    
    func (s *SafeMap) Set(key string, value int) {
        s.mu.Lock() // Блокировка на запись
        defer s.mu.Unlock()
        s.m[key] = value
    }
  2. sync.Map: Специализированный тип, добавленный в Go 1.9. Он оптимизирован для сценариев, где ключ после записи редко изменяется или удаляется, а количество чтений велико. sync.Map позволяет избежать глобальной блокировки для большинства операций чтения, что делает его производительнее map с мьютексом в определенных условиях.

Ответ 18+ 🔞

А, ну ты про это, про мапы в гоурутинах! Да, тут история простая, как три копейки, но если накосячить — будет пиздец, а не программа.

Смотри, обычная мапа в Go — это как тарелка с оливье на новогоднем столе, к которой лезут десять пьяных дядек. Один вилку ткнул, другой уже ложкой херачит, третий вообще рукой лезет. В итоге — половина салата на полу, майонез на шторах, а кто-то в итоге получит в морду вилкой. Так и с мапой: если несколько горутин начинают в неё писать и читать без спросу, runtime может просто охуеть и выдать панику fatal error: concurrent map read and map write. И всё, приехали.

Что делать-то? Варианта два, оба с приколами.

Первый — классика, мьютексы. Берёшь sync.RWMutex, оборачиваешь им свою мапу в структуру и делаешь методы. Это как поставить бугая у той самой тарелки с оливье. Хочешь взять — спросись, хочешь положить — тем более. RWMutex хорош тем, что пока никто не пишет, много народу может читать одновременно. Но как только кто-то собрался записать — все читающие ждут, писатель один работает. В коде это выглядит примерно так:

type SafeMap struct {
    mu sync.RWMutex // Вот наш бугай, охраняющий порядок
    m  map[string]int
}

func (s *SafeMap) Get(key string) (int, bool) {
    s.mu.RLock() // Бугай, я только посмотреть! (блокировка на чтение)
    defer s.mu.RUnlock() // Посмотрел, отпускаю
    val, ok := s.m[key]
    return val, ok
}

func (s *SafeMap) Set(key string, value int) {
    s.mu.Lock() // Бугай, я сейчас новую селёдку под шубой добавлю! (блокировка на запись)
    defer s.mu.Unlock()
    s.m[key] = value
}

Работает надёжно, проверено веками. Но если у тебя операции с мапой — это адский хотспот, где все горутины только тем и заняты, что пишут и читают, то бугай-мьютекс может стать узким горлышком. Все будут ждать своей очереди к тарелке, а праздник уже кончился.

Второй вариант — sync.Map. Это уже не просто тарелка, а какая-то хитрая японская штука с конвейером. Его добавили в Go 1.9 для очень конкретных сценариев. Он охуенно быстр, когда у тебя: 1) Ключи в основном пишутся один раз, а потом только читаются (типа кэша конфигурации). 2) Или когда у тебя кучу горутин работают с совершенно разными наборами ключей.

Внутри он умный, там нет одной большой блокировки на всю мапу, поэтому в этих специфичных случаях он летает. Но! Он идиоматически не такой, как обычная мапа. Методы другие: Load, Store, LoadOrStore, Range. И если тебе нужны, например, длина мапы или итерация по всем ключам — с sync.Map это не так просто.

var m sync.Map
m.Store("answer", 42) // Записали
value, ok := m.Load("answer") // Прочитали

Короче, выбор такой:

  • map + sync.RWMutex — твой универсальный солдат, понятный, предсказуемый. Подходит для большинства случаев. Если не уверен — бери его.
  • sync.Map — это спецназ. Бери только если точно понимаешь, что у тебя один из тех самых узких сценариев, где он выжмет все соки. Иначе можешь получить хуже производительность и головную боль.

Вот и вся магия, ёпта. Главное — не лезь в общую мапу из разных горутин без защиты, а то будет не "му-му", а полный пиздец.