Data race — две горутины трогают одну переменную, среди доступов есть запись, и нет договорённости «кто раньше». Это баг, а не «на моей машине работает».
Конкурентность Go golang собеседование
Гонка данных в Go определена жёстко. Две горутины трогают одну переменную (или элемент слайса, поле структуры). Хотя бы один доступ — запись. Между ними нет синхронизации. Тогда компилятор и процессор вольны переставлять чтения и записи. Симптомы любые: потерянный инкремент, порванный заголовок слайса, «невозможное» значение, краш.
Это не «два HTTP-запроса» и не «медленный GC». Два запроса могут спокойно работать с разными данными. Гонка — про память без барьера. Канал, mutex, WaitGroup, Once, atomic, запуск горутины — как раз такие барьеры.
Аналогия: двое одновременно пишут на одну и ту же клетку доски. Без правила «сначала я, потом ты» получится каша. Даже если «почти всегда» выходит красиво.
Что такое конфликт
Чтение плюс чтение без записи — не data race. Запись плюс запись или запись плюс чтение без синхронизации — да. Даже «я только инициализирую до старта горутин» нужно доказать: все записи видны до go, дальше только чтение, или снова синхронизация.
// go test -race ./...
// go run -race .
var n int
func bad() {
go func() { n++ }()
go func() { n++ }() // data race на n
}
func good() {
var mu sync.Mutex
go func() {
mu.Lock()
n++
mu.Unlock()
}()
}
Детектор гонок на тестах. Unlock гарантирует, что следующий Lock увидит записи.
Пример. Две горутины делают n++ без замка. Это data race. На практике можно потерять единицу, увидеть странное значение или словить краш. «На x86 инкремент атомарный» модель Go не отменяет.
На CI включают -race. Детектор не бесплатен по CPU и памяти, в проде его редко держат постоянно. Он ловит не все гонки: редкие ветки и тайминги. Но это основной инструмент, не опция «для перфекционистов».
Когда запись точно видна
Модель памяти говорит, какие записи гарантированно видны. Коротко: «A happens-before B» значит «если A закончилась, B обязана увидеть всё, что A записала». Unlock мьютекса так связан со следующим Lock той же копии. Send в канал — с соответствующим receive. Закрытие канала — с приёмом нуля из него. Старт горутины — с её телом. Наоборот нет: выход из горутины сам по себе ничего не синхронизирует, пока нет WaitGroup или канала.
Пример. Горутина записала cfg = loaded и сделала close(done). Другая делает <-done и потом читает cfg. Закрытие канала даёт видимость: cfg уже loaded. Без close или mutex вторая может увидеть нуль.
Atomic даёт упорядочивание на своей переменной. Обычный int++ без atomic и mutex — гонка даже на одно слово. Разные ключи одной map всё равно требуют синхронизации: общая сама таблица.
Типичная ошибка
Определить гонку как «два потока». Нужны конфликтующие доступы к памяти без синхронизации, и среди них есть запись. Игнорировать -race на CI. Считать, что разные ключи map не требуют лока. «Потом только читаем» — тоже гонка, если запись не синхронизирована со стартом читателей.
На собеседовании
Формулировка: конфликтующие доступы, есть запись, нет синхронизации. Инструмент — go test -race. Unlock затем Lock, send затем receive, close затем приём нуля — базовые гарантии видимости. Это баг, не «флаки».