Как ограничить количество одновременно выполняющихся горутин?

Ответ

Да, для ограничения количества одновременно работающих горутин в Go обычно используются паттерны, основанные на каналах или специальных примитивах синхронизации.

1. Буферизированный канал (Паттерн "Семафор")

Это классический и самый простой способ. Канал с буфером используется как пул "разрешений" на запуск. Горутина может запуститься, только когда получит значение из канала.

2. Пакет golang.org/x/sync/semaphore

Этот пакет предоставляет более явную и читаемую реализацию семафора.

import (
    "context"
    "golang.org/x/sync/semaphore"
    "sync"
)

// ...

var wg sync.WaitGroup
maxWorkers := int64(5)
sem := semaphore.NewWeighted(maxWorkers)
ctx := context.Background()

for i := 0; i < 20; i++ {
    wg.Add(1)
    // Запрашиваем "вес" 1. Блокирует, если семафор исчерпан.
    if err := sem.Acquire(ctx, 1); err != nil {
        // Обработка ошибки, если контекст отменен
        wg.Done()
        continue
    }

    go func(n int) {
        defer wg.Done()
        defer sem.Release(1) // Освобождаем "вес"
        // Полезная работа
    }(i)
}

wg.Wait()

3. Пакет golang.org/x/sync/errgroup

Для задач, где нужно не только ограничить параллелизм, но и обрабатывать ошибки, errgroup является идеальным решением. Метод SetLimit позволяет легко задать максимальное количество горутин.

import "golang.org/x/sync/errgroup"

// ...

g, ctx := errgroup.WithContext(context.Background())
g.SetLimit(5) // Устанавливаем лимит

for i := 0; i < 20; i++ {
    job := i
    g.Go(func() error {
        // Полезная работа, которая может вернуть ошибку
        return doWork(ctx, job)
    })
}

// Ожидаем завершения и получаем первую возникшую ошибку
if err := g.Wait(); err != nil {
    log.Fatal(err)
}