Как избежать проблем с конкурентным доступом при работе с общими данными в goroutine?
Что проверяют
Знаете ли вы, что такое гонка данных в модели памяти Go, и умеете ли выбрать инструмент под задачу, а не хвататься за мьютекс везде. Следом почти всегда спрашивают, как найти гонку, которую глазами не видно, и когда канал лучше мьютекса.
Как отвечать
Начните с определения: гонка возникает, когда две горутины обращаются к одной переменной, хотя бы одна пишет, и между обращениями нет отношения happens-before. Результат не определён, а на обычной map рантайм просто падает с fatal error: concurrent map writes. Дальше инструменты:
sync.Mutexиsync.RWMutex: защищают участок кода.RWMutexвыигрывает только при сильном перекосе в чтение, иначе он дороже обычного;- каналы: передают владение данными. Одна горутина владеет состоянием, остальные шлют ей сообщения, и общей памяти нет вовсе;
sync/atomic: счётчики и флаги. С Go 1.19 есть типыatomic.Int64,atomic.Bool,atomic.Pointer[T], их сложнее применить неправильно, чем функции вродеatomic.AddInt64;sync.Once,sync.Map,errgroup: ленивая инициализация, map для узких сценариев (ключ пишется один раз и много читается), группа горутин с общей ошибкой и отменой через контекст;- неизменяемость: данные, которые после создания никто не меняет, синхронизации не требуют. Иногда проще отдать горутине копию, чем защищать общий объект.
Обязательно назовите детектор гонок: go test -race или go run -race. Он видит только те пути, которые реально выполнились, поэтому тесты должны нагружать конкурентные сценарии, а не проходить их по одному.
Где спотыкаются
Копируют структуру с мьютексом по значению: метод с получателем не по указателю блокирует копию, и защита исчезает. go vet ловит это проверкой copylocks.
Защищают запись, а читают без блокировки, считая чтение безопасным. Чтение без синхронизации параллельно с записью это та же гонка.
Вызывают wg.Add(1) внутри запущенной горутины, а не до её старта. Wait успевает вернуться раньше, чем горутина вообще начала работу.