В прошлый раз мы разобрали, как буферизованный канал работает как семафор. Это хороший паттерн, но у него есть потолок: нет встроенной поддержки контекста, нет возможности задать разный «вес» для разных операций. Пакет
golang.org/x/sync закрывает оба этих пробела.Что внутри
Пакет предоставляет взвешенный семафор
semaphore.NewWeighted. Принцип тот же: ограничиваете число параллельных операций. Но теперь каждая операция может занимать не один слот, а несколько. И Acquire умеет слушать контекст.Базовое использование:
import "golang.org/x/sync/semaphore"
var (
maxWorkers = int64(10)
sem = semaphore.NewWeighted(maxWorkers)
)
func processAll(ctx context.Context, tasks []Task) error {
for _, task := range tasks {
if err := sem.Acquire(ctx, 1); err != nil {
return fmt.Errorf("semaphore acquire: %w", err)
}
go func(t Task) {
defer sem.Release(1)
process(ctx, t)
}(task)
}
// Ждём завершения всех горутин
if err := sem.Acquire(ctx, maxWorkers); err != nil {
return fmt.Errorf("final acquire: %w", err)
}
sem.Release(maxWorkers)
return nil
}
Два момента, на которые стоит обратить внимание.
Acquire возвращает ошибку, если контекст отменён или истёк дедлайн. Горутины не зависают — они получают сигнал и завершаются. Это именно то, чего не хватало буферизованному каналу.Финальный
Acquire(ctx, maxWorkers) — это трюк для ожидания. Мы пробуем занять все слоты сразу. Это возможно только тогда, когда все горутины завершились и освободили свои слоты. Как только захват прошёл — сразу отпускаем и выходим.Взвешенный захват: зачем он нужен
Не все задачи одинаково нагружают систему. Загрузка большого файла потребляет куда больше ресурсов, чем быстрый запрос в базу.
Взвешенный семафор моделирует напрямую:
// Тяжёлая операция занимает 3 слота
if err := sem.Acquire(ctx, 3); err != nil {
return err
}
go func() {
defer sem.Release(3)
heavyOperation(ctx)
}()
// Лёгкая операция занимает 1 слот
if err := sem.Acquire(ctx, 1); err != nil {
return err
}
go func() {
defer sem.Release(1)
lightOperation(ctx)
}()
При лимите в 10 слотов одновременно могут работать либо 10 лёгких операций, либо 3 тяжёлых, либо любая комбинация. Вы управляете не просто количеством горутин, а их суммарным потреблением ресурсов.
Когда использовать
Буферизованный канал подходит для скриптов и простых утилит, где все задачи равнозначны и контекст не нужен.
golang.org/x/sync/semaphore — выбор для продакшена: корректно обрабатывает отмену, не теряет горутины при завершении и позволяет точнее контролировать нагрузку.📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#GoToProduction