> Почему при эвакуации данных в мапе Go возникает проблема с конкурентным доступом и race condition (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Т-Банк

Стек: Go

> Пример ответа

Короткий ответ

Проблема возникает из-за того, что встроенная мапа в Go не является thread-safe: одновременные чтение и запись, или две записи в один и тот же bucket, приводят к data race. При эвакуации (rehashing) мапа увеличивает количество bucket'ов и перераспределяет элементы, что включает запись в структуры данных, которые в этот момент могут читаться другими горутинами. Go runtime детектит это через race detector и паникует с fatal error: concurrent map read and map write.

Подробное объяснение

Внутренняя структура мапы в Go - это хеш-таблица с bucket'ами. Каждый bucket хранит до 8 пар ключ-значение. Когда коэффициент заполнения превышает порог (6.5), мапа запускает эвакуацию: создаётся новый массив bucket'ов в два раза больше, и элементы постепенно переносятся из старых bucket'ов в новые.

Эвакуация - это не атомарная операция. Она выполняется инкрементально: при каждой вставке или удалении переносится небольшое количество элементов. В этот момент мапа находится в промежуточном состоянии: часть данных уже в новых bucket'ах, часть ещё в старых. Любая горутина, которая читает или пишет в мапу во время этого процесса, может:

  • прочитать элемент из старого bucket'а, который уже был перенесён - получит устаревшие данные;
  • записать в старый bucket, который уже помечен как evacuated - данные будут потеряны;
  • одновременно с эвакуацией изменить структуру bucket'а, что приведёт к повреждению внутренних указателей.

Go runtime не блокирует доступ к мапе во время эвакуации - это сделано для производительности. Вместо этого он полагается на то, что доступ к мапе из нескольких горутин будет синхронизирован на уровне приложения (через mutex, channel или atomic). Если синхронизации нет, race detector фиксирует гонку, и программа либо падает с fatal error, либо выдаёт некорректные результаты.

На практике

Для конкурентного доступа к данным используй sync.RWMutex (если чтений больше, чем записей), sync.Map (для специфичных паттернов: append-only, disjoint keysets), или структуры данных из golang.org/x/sync (например, Map с кастомной стратегией). Никогда не полагайся на то, что мапа "сама разберётся" - race condition может не проявиться на низкой нагрузке, но выстрелит в production.

Пример кода

GO
// Плохо: data race при эвакуации
var m map[string]int
func write() {
for i := 0; i < 1000; i++ {
m[fmt.Sprintf("key%d", i)] = i
}
}
func read() {
for i := 0; i < 1000; i++ {
_ = m[fmt.Sprintf("key%d", i)]
}
}
// go run -race main.go → fatal error: concurrent map read and map write
// Хорошо: синхронизация через RWMutex
var (
mu sync.RWMutex
m = make(map[string]int)
)
func safeWrite(key string, val int) {
mu.Lock()
defer mu.Unlock()
m[key] = val
}
func safeRead(key string) int {
mu.RLock()
defer mu.RUnlock()
return m[key]
}

Как отвечать на собеседовании

Начни с того, что мапа в Go не thread-safe по дизайну. Затем объясни механизм эвакуации: когда она запускается, как происходит инкрементальный перенос. Подчеркни, что проблема не только в эвакуации, но и в любой одновременной записи и чтении - эвакуация лишь усугубляет ситуацию, потому что затрагивает внутреннюю структуру. Упомяни, что runtime детектит гонку и паникует, но это не гарантировано - иногда данные просто портятся. В конце предложи решения: mutex, sync.Map, шардирование.

Что проверяет интервьюер

  • Понимание внутреннего устройства мапы (bucket, load factor, incremental evacuation).
  • Осознание разницы между data race и race condition.
  • Знание инструментов: -race, go vet.
  • Умение выбирать правильный примитив синхронизации под задачу.
  • Понимание trade-off между производительностью и безопасностью.

Типичные ошибки

  • Утверждение, что проблема только в эвакуации - на самом деле гонка возможна и без неё, при любой одновременной записи.
  • Предложение использовать sync.Map как универсальное решение - он эффективен только для определённых паттернов.
  • Игнорирование того, что race detector не всегда срабатывает - он вероятностный, поэтому код может работать локально и падать в production.
  • Совет "просто не использовать мапы" - это не решение, нужно правильно синхронизировать доступ.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью