Сначала уточнение, за которое дают плюс на собесе: важно понять, что именно ограничиваем.
Concurrency limit — сколько запросов летит одновременно (не больше 10 в моменте). Примитив — семафор.
Rate limit — сколько запросов в единицу времени (100 rps). Примитив — token bucket, x/time/rate.
Их часто путают. «Не больше 10 параллельных» — первое. «Не больше 100 в секунду» — второе. Дальше про concurrency.
Хороший выбор — семафор. В Go проще всего сделать его на буферизированном канале: канал ёмкости N — это счётчик свободных слотов:
sem := make(chan struct{}, 10)
for _, req := range requests {
sem <- struct{}{} // заняли слот
go func(r Request) {
defer func() { <-sem }() // освободили
callExternalAPI(r)
}(req)
}struct{} — потому что нужен только факт занятости, он весит 0 байт. Канал даёт и счётчик, и блокировку сразу.На практике берут
errgroup.SetLimit — тот же семафор, но с ошибками, Wait и отменой контекста из коробки:g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10)
for _, req := range requests {
req := req
g.Go(func() error {
return callExternalAPI(ctx, req)
})
}
return g.Wait()
🐸 Библиотека Go для собеса