Как реализовать паттерн fan-out fan-in в Go с использованием горутин?
Что проверяют
Умеете ли вы строить конвейер на каналах так, чтобы он корректно завершался: без дедлоков, утечек горутин и паники на закрытом канале. Следом почти всегда просят добавить отмену через context и спрашивают, что будет с порядком результатов.
Как отвечать
Разведите два термина. Fan-out: несколько горутин читают из одного входного канала и делят работу. Fan-in: результаты из нескольких каналов сливаются в один. Каркас из трёх частей:
- генератор: горутина пишет задачи в канал
inи закрывает его, когда задачи кончились. Закрывает всегда отправитель, а не получатель; - воркеры: N горутин делают
for v := range inи пишут в свой канал результатов. Число N ограничено: по числу ядер для CPU-задач, по лимиту внешнего сервиса для сетевых; - merge: для каждого входного канала горутина пересылает значения в общий
out,sync.WaitGroupсчитает эти горутины, и отдельная горутина послеwg.Wait()закрываетout. Только после этогоrange outу потребителя завершится.
Каждую отправку оборачивайте в select с веткой case <-ctx.Done(). Иначе, если потребитель ушёл по ошибке, воркеры навсегда повиснут на записи в канал, и это утечка. В реальном коде того же добиваются через errgroup из golang.org/x/sync: errgroup.WithContext отменяет контекст при первой ошибке, а SetLimit ограничивает число одновременных горутин. В Go 1.25 у WaitGroup появился метод Go, который сам делает Add и Done.
Скажите про порядок: fan-in его не сохраняет. Если он нужен, передавайте индекс вместе с результатом и раскладывайте в слайс по индексу.
Где спотыкаются
Закрывают out внутри воркера. Второй воркер пишет в закрытый канал, и программа падает с паникой. Закрывать должен один владелец после того, как все писатели закончили.
Вызывают wg.Wait() в главной горутине до чтения результатов, когда out без буфера или буфер меньше числа результатов. Воркеры ждут читателя, читатель ждёт воркеров, получается дедлок.
Не думают об отмене: без ctx при раннем выходе потребителя горутины висят до конца жизни процесса, и в долгоживущем сервисе это видно по росту runtime.NumGoroutine().