> Используете ли вы воркерпул для ограничения количества горутин в Go (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Wildberries

Стек: Go

> Пример ответа

Короткий ответ

Да, использую. Воркерпул - стандартный паттерн для ограничения конкурентности, когда число задач значительно превышает разумное количество параллельных горутин. Это защищает от неконтролируемого роста потребления памяти, дескрипторов и нагрузки на планировщик. Реализую через каналы задач и фиксированное число воркеров, либо через semaphore-паттерн с буферизованным каналом. Выбор зависит от сценария: пакетная обработка, стриминг или long-lived сервис.

Подробное объяснение

Воркерпул решает две задачи: ограничение максимального числа одновременно работающих горутин и переиспользование ресурсов. Без него каждая задача порождает горутину - при пиковой нагрузке это приводит к деградации из-за переключения контекста и роста heap.

Ключевые элементы:

  • очередь задач - канал с задачами (обычно chan Job), буферизованный или нет;
  • воркеры - фиксированное число горутин, читающих из канала;
  • управление завершением - закрытие канала задач и sync.WaitGroup для ожидания воркеров.

Важный trade-off: размер пула. Слишком маленький - задачи копятся в очереди, растёт latency. Слишком большой - теряется смысл ограничения. Оптимальный размер зависит от характера работы: I/O-bound задачи позволяют больше воркеров, CPU-bound - ограничиваются runtime.GOMAXPROCS.

Альтернатива воркерпулу - semaphore-паттерн через буферизованный канал: каждая задача запускает горутину, но перед стартом захватывает слот. Это проще, но не даёт переиспользования горутин и контроля над очередью.

На практике

В production-коде предпочитаю воркерпул для пакетной обработки (например, обогащение данных из внешних API) и semaphore для случаев, когда задачи приходят стримингом и не требуют очереди. Важно продумать:

  • graceful shutdown: воркеры должны завершиться после обработки всех задач из очереди;
  • обработку ошибок: ошибка воркера не должна ронять весь пул;
  • контекст для отмены: context.Context в каждой задаче.

Пример кода

GO
type Job func(ctx context.Context) error
func WorkerPool(ctx context.Context, jobs <-chan Job, workers int) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var wg sync.WaitGroup
errCh := make(chan error, workers)
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
if err := job(ctx); err != nil {
errCh <- err
cancel() // останавливаем остальные воркеры
return
}
}
}()
}
wg.Wait()
close(errCh)
select {
case err := <-errCh:
return err
default:
return nil
}
}

Использование:

GO
jobs := make(chan Job, 100)
go func() {
defer close(jobs)
for _, item := range items {
jobs <- func(ctx context.Context) error {
return processItem(ctx, item)
}
}
}()
err := WorkerPool(ctx, jobs, 10)

Как отвечать на собеседовании

Начни с прямого ответа - да, использую. Затем коротко объясни зачем: ограничение конкурентности, защита ресурсов. Упомяни два подхода - воркерпул и semaphore - и когда какой выбираешь. Приведи пример структуры: канал задач, фиксированное число воркеров, WaitGroup. Обязательно затронь graceful shutdown и обработку ошибок - это показывает опыт. Если спросят про размер пула, скажи, что зависит от типа нагрузки: для I/O-bound можно больше, для CPU-bound - около GOMAXPROCS.

Что проверяет интервьюер

  • понимание модели конкурентности Go: горутины дёшевы, но не бесплатны;
  • умение проектировать конкурентные паттерны, а не просто знать синтаксис;
  • внимание к деталям: закрытие каналов, утечки горутин, обработка ошибок;
  • способность обосновать выбор между воркерпулом и semaphore;
  • знание sync.WaitGroup, context, буферизованных каналов.

Типичные ошибки

  • создавать горутину на каждую задачу без ограничений - рано или поздно упрётесь в память;
  • забыть закрыть канал задач - воркеры зависнут навсегда;
  • не использовать WaitGroup - главная горутина завершится раньше воркеров;
  • игнорировать ошибки воркеров - пул продолжает работать при первой же ошибке;
  • выбирать размер пула без учёта типа нагрузки, например, ставить 1000 воркеров для CPU-bound задач;
  • не использовать context - невозможно корректно остановить пул при завершении программы.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью