Конкурентность в Go
Конкурентность в Go строится из goroutine, каналов, примитивов sync, атомарных операций и явного управления жизненным циклом. Корректность начинается не с выбора примитива, а с контракта: кто владеет данными, кто останавливает работу и какое событие устанавливает порядок между операциями.
Зачем это на интервью
Нужно показать, что вы отличаете параллелизм от конкурентности, умеете не допускать data race и утечек goroutine, а также выбираете простой способ синхронизации под инвариант, а не «каналы везде».
Темы
- Жизненный цикл goroutine и модель G-M-P
- Каналы: ownership, send/receive и close
- select, blocking, default и nil-каналы
- sync: Mutex, RWMutex, WaitGroup, Once, Cond, Pool и Map
- Атомарные операции и модель памяти Go
- Data race, deadlock, starvation и race detector
- Fan-out, fan-in и worker pool
- Pipeline и отмена
- Semaphore и rate limiting
- Graceful shutdown
- Конкурентные задачи на интервью
Минимум для E4
- Для общего изменяемого состояния выбирать
Mutexлибо последовательного владельца данных, а не читать/писать map одновременно. - Уметь объяснить блокировки send/receive,
close,selectи отмену черезcontext.Context. - Запускать тесты с
go test -race ./...и понимать, что отсутствие отчёта не является доказательством отсутствия гонок. - Не оставлять goroutine без пути к завершению и не закрывать канал со стороны получателя.
Углубление для E5/Senior
Устойчивый дизайн задаёт границы параллелизма, backpressure, ownership и наблюдаемое завершение до реализации. При нагрузке оценивают не только throughput: нужны очередь, p95/p99, число goroutine, block/mutex profile и причины отмены. Оптимизацию синхронизации начинают после профилирования, сохраняя ясный инвариант.
Ключевые понятия
- Concurrency — организация независимых работ с перекрытием ожиданий; parallelism — их одновременное выполнение на CPU.
- Happens-before — гарантированный порядок, делающий запись видимой последующему чтению.
- Ownership — компонент, единолично ответственный за изменение или закрытие ресурса.
- Backpressure — ограничение производителя скоростью потребителя через буфер, семафор или отмену.
Типовые вопросы
- Когда выбрать канал, а когда mutex?
- Канал удобен для передачи работы и владения; mutex — для защиты небольшого общего инварианта. Выбор определяется потоком данных и стоимостью модели, а не идеологией.
- Что гарантирует
go f()?- Только запуск новой goroutine; вызывающий код не ждёт её завершения и должен сам задать синхронизацию.
- Кто закрывает канал?
- Отправитель, который единственный знает, что новых значений не будет; при нескольких отправителях нужен единый владелец/координатор.
- Почему
-raceнедостаточен?- Детектор видит лишь реально выполненные конфликтующие обращения. Нужны достаточные тестовые сценарии и корректный дизайн.
- Что делать при росте числа goroutine?
- Снять goroutine profile/trace, найти точку ожидания и добавить cancellation, deadline, закрытие входов либо лимит параллелизма.
Практика
- Реализуйте обработчик задач с фиксированным лимитом workers и
context-отменой. Готово: при отмене все goroutine завершаются, нет send в заблокированный выход, тест проходитgo test -race. - Сделайте намеренную гонку счётчика и исправьте её двумя способами. Готово: объяснён инвариант,
-raceна сценарии находит ошибку до исправления и молчит после него. - Снимите
go tool pprofgoroutine/mutex profile нагруженной программы. Готово: названа причина самого долгого ожидания и измерен эффект исправления.
Частые ошибки и ловушки
- Запускать goroutine без условия остановки, deadline или получателя результата.
- Закрывать канал из нескольких мест либо закрывать его при продолжающихся send.
- Использовать
time.Afterв горячем цикле вместо управляемого таймера и отмены. - Копировать значение с
Mutex,WaitGroupилиOnceпосле первого использования. - Подменять измерение конкурентности количеством goroutine.
Связанные темы
Go · Context и жизненный цикл · Runtime и управление памятью · Наблюдаемость · Тестирование