Паттерны конкурентности, практика руками и три класса проблем
Вторая часть конспекта по конкурентности — типовые паттерны построения конкурентного кода (worker pool, pipeline, fan-in/fan-out) с рабочим кодом на каждый, формат живой практики "напиши руками за 15-20 минут" (те же паттерны + балансировщик), и три класса проблем, которые в них легко допустить.
Паттерны: worker pool, pipeline, fan-in/fan-out
Worker pool — фиксированное число N горутин читают задачи из общего канала параллельно. Ограничивает степень параллелизма и переиспользует горутины вместо запуска новой на каждую задачу.
Внутри воркера нужны ДВА select (не один!) — один на получение задачи (с ctx.Done()), второй на отправку результата (тоже с ctx.Done()): без второго воркер зависнет навсегда на блокирующей записи результата, если читатель результатов уже ушёл из-за отмены. Частая ошибка: закрыть канал результатов сразу после запуска воркеров, а не после того, как ВСЕ они реально закончили (wg.Wait() в отдельной горутине перед close) — иначе паника send on closed channel у воркера, который ещё пишет.
Pipeline — цепочка стадий, каждая своя горутина (или пул), соединённых каналами: выход одной — вход следующей. Данные текут потоково, не дожидаясь обработки ВСЕХ элементов на предыдущей стадии — снижает пиковую память.
Fan-out — один источник задач раздаётся нескольким обработчикам (то же самое, что делает worker pool выше: несколько воркеров читают из одного канала jobs). Fan-in — обратное: результаты нескольких каналов сливаются в один выходной, обычно после fan-out, чтобы собрать параллельную работу обратно вместе.
Практика: worker pool, балансировщик, pipeline руками
Формат: чистый лист, 15–20 минут, без подсказок — ровно как на секции по Go в Avito/Ozon/Яндексе. Разница между «понимаю каналы» и «умею писать конкурентный код» проверяется именно так — worker pool и pipeline руками смотри выше, здесь третий типовой формат той же практики: балансировщик.
В балансировщике с round-robin+retry под давлением времени легко забыть про потокобезопасность индекса текущего backend'а (нужен Mutex или atomic-счётчик), и про то, что нужно возвращать итоговую ошибку, если ВСЕ backend'ы отказали, а не зависать/молчать.
В pipeline (см. код выше) — классическая ловушка: читать входной канал через for chunk := range chunks вместо select с ctx.Done() — range не видит отмену контекста и не разблокируется, пока канал не закроют.
Интервьюер почти всегда спрашивает ПОСЛЕ того, как код написан: «а что если один backend не ошибку вернёт, а просто зависнет навсегда?» — прямая проверка, обёрнут ли вызов backend в select с ctx.Done(), а не просто вызван напрямую.
Утечка горутин, deadlock, race condition — три разных класса проблем
На собеседовании важно называть точный термин, а не «что-то не так с горутинами» — это три РАЗНЫХ класса проблем, которые часто путают между собой.
Утечка горутины — горутина заблокирована навсегда (обычно на отправке в канал, который никто не читает). Утечки накапливаются, каждая держит стек и ресурсы, пока процесс не упадёт по OOM. Профилактика: у КАЖДОЙ горутины должен быть явный гарантированный способ завершиться.
Deadlock — две+ горутины взаимно ждут друг друга, ПОЛНАЯ остановка. Классическая причина — захват нескольких мьютексов в разном порядке разными горутинами (ABBA-deadlock). Go детектирует deadlock, только если ВСЕ горутины программы заблокированы одновременно (паника all goroutines are asleep) — частичный deadlock (часть горутин продолжает работать) рантайм не ловит.
Race condition — конкурентный доступ к памяти без синхронизации, где хотя бы одно обращение — запись. counter++ — это чтение+инкремент+запись, не одна атомарная операция; результат непредсказуем, но программа НЕ падает и НЕ виснет (в отличие от двух пунктов выше) — просто даёт неверный ответ.
Ключевой факт про инструменты: go run -race находит гонки данных, но НЕ находит deadlock — это разные классы проблем с разной диагностикой. Конкурентная запись в обычную (не sync.Map) map без синхронизации — не тихая гонка, а немедленная паника рантайма fatal error: concurrent map writes, в отличие от гонки на простом int-счётчике, где часть инкрементов просто молча теряется.