Как реализовать worker pool для ограничения количества горутин в Go
Что проверяют
Умеете ли вы ограничить параллелизм без утечек горутин и дедлоков и понимаете ли, кто и когда закрывает каналы. Следом обычно спрашивают про отмену через context и обработку ошибок из задач.
Как отвечать
Классическая схема: канал задач, N воркеров и sync.WaitGroup.
- воркеры: запускаются заранее, каждый крутит
for job := range jobsи выходит сам, когда канал закрыт; - закрытие: канал
jobsзакрывает только отправитель, после того как отдал все задачи. Канал результатов закрывает отдельная горутина послеwg.Wait(), иначе читатель зависнет; - отмена: воркер делает
selectпоctx.Done()и каналу задач, отправка результата тоже идёт черезselect, чтобы воркер не застрял, когда результаты больше никто не читает; - размер пула: для CPU-задач ориентир
runtime.GOMAXPROCS(0), для задач с сетью или базой предел задаёт внешний ресурс: пул соединений, лимит API.
Если пул как отдельная сущность не нужен, есть способы короче. Семафор на буферизованном канале: sem := make(chan struct{}, n), перед запуском горутины запись в канал, в defer чтение. Или errgroup из golang.org/x/sync с g.SetLimit(n): он ограничит число горутин, вернёт первую ошибку и отменит контекст остальным. Для задач разного веса там же есть semaphore.Weighted. С Go 1.25 у WaitGroup появился метод Go, который сам делает Add и Done.
Упомяните панику: паника в горутине без recover роняет весь процесс, поэтому воркер, выполняющий чужой код, оборачивает задачу в defer с recover. А копия i := i в цикле с Go 1.22 не нужна, переменная цикла своя на каждой итерации.
Где спотыкаются
Закрывают канал со стороны читателя или из нескольких горутин. Отправка в закрытый канал и повторное закрытие дают панику.
Вызывают wg.Add(1) внутри запускаемой горутины. Wait может выполниться раньше, чем счётчик увеличился, и программа завершится, не дождавшись работы.
Ставят wg.Done() последней строкой, а не через defer. Ранний return или перехваченная паника оставляют счётчик ненулевым, и Wait висит вечно.