Как решить проблему записи в map из нескольких горутин в Go
Что проверяют
Знает ли кандидат, что встроенный map в Go не потокобезопасен и что рантайм это не прощает, а также умеет ли выбрать защиту под профиль нагрузки: мьютекс, шардирование, sync.Map или владелец через канал.
Как отвечать
Начать с точной формулировки: одновременная запись в map из нескольких горутин, как и запись параллельно с чтением, это не «неопределённое поведение», а фатальная ошибка рантайма. Начиная с Go 1.6 в хеш-таблице есть флаг записи, и при обнаружении гонки процесс падает с fatal error: concurrent map writes или concurrent map read and map write. Это не паника: recover() её не перехватывает, процесс умирает целиком. Параллельные чтения без записей безопасны.
Основное решение: инкапсулировать map вместе с мьютексом.
type Store struct {
mu sync.RWMutex
data map[int]int
}
func NewStore() *Store {
return &Store{data: make(map[int]int)}
}
func (s *Store) Set(k, v int) {
s.mu.Lock()
defer s.mu.Unlock()
s.data[k] = v
}
func (s *Store) Get(k int) (int, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
v, ok := s.data[k]
return v, ok
}Тип должен передаваться указателем: копия структуры копирует и sync.Mutex, а копировать его нельзя (go vet на это ругается). Метод, возвращающий саму map наружу, ломает всю защиту, потому что вызывающий получит доступ без блокировки: наружу отдаются копии или значения.
Чем выбирать дальше:
sync.Mutexпротивsync.RWMutex.RWMutexвыигрывает только при явном перевесе чтений и достаточно долгих критических секциях. На коротких операциях и большом числе ядер он проигрывает обычномуMutexиз-за конкуренции за общий счётчик читателей, так что решать надо бенчмарком, а не по вкусу.sync.Mapдокументирован под два сценария: ключ пишется один раз и читается многократно, либо горутины работают с непересекающимися наборами ключей. Это не универсальная замена: API нетипизированный (any, приведения в рантайме), нетlen, обход черезRangeбез гарантий консистентного снимка. В Go 1.24 внутренности переписали на HashTrieMap, производительность заметно выросла, но область применения та же.- Шардирование для высокой конкуренции: массив из N структур
{mu, map}, шард выбирается хешем ключа. Снижает борьбу за один замок почти линейно, ценой того, что операции над всей коллекцией (подсчёт, обход) становятся дороже. - Владелец через канал: одна горутина держит map и обслуживает запросы из канала. Просто и очевидно, но пропускная способность упирается в одну горутину.
Проверять гонки надо go test -race и go build -race на стенде: детектор ловит проблему до того, как она уронит прод.
Где спотыкаются
Считают, что чтение во время записи безопасно, и защищают мьютексом только Set. Второе: берут sync.Map по умолчанию «потому что он потокобезопасный», хотя при смешанной нагрузке с частыми обновлениями существующих ключей обычный map под мьютексом обычно быстрее.