Как ограничить количество *одновременных* запросов к обработчику, используя только стандартные средства Go?

Ответ

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

Это не rate limiting (ограничение частоты), а concurrency limiting (ограничение параллелизма).

1. Использование буферизованного канала

Канал используется как семафор: размер буфера — это максимальное количество одновременных выполнений. Запись в канал "захватывает" слот, чтение — "освобождает".

// Канал с буфером на 10 элементов. 
// Позволит выполняться не более чем 10 обработчикам одновременно.
var sem = make(chan struct{}, 10)

func handler(w http.ResponseWriter, r *http.Request) {
    // Попытка записать в канал. Если он полон, горутина заблокируется.
    sem <- struct{}{}
    // defer гарантирует, что слот будет освобожден после завершения функции.
    defer func() { <-sem }()

    // Логика обработки запроса
    time.Sleep(2 * time.Second)
    w.Write([]byte("Request processed successfully"))
}

2. Использование golang.org/x/sync/semaphore

Хотя этот пакет не входит в стандартную библиотеку, он является официальным подрепозиторием Go и предоставляет более гибкий и явный API для семафоров.

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

// Семафор с "весом" 10. Каждый запрос будет "весить" 1.
var sem = semaphore.NewWeighted(10)

func handler(w http.ResponseWriter, r *http.Request) {
    // Пытаемся захватить семафор. Учитываем контекст запроса для отмены.
    if err := sem.Acquire(r.Context(), 1); err != nil {
        // Не удалось захватить (например, контекст отменен)
        http.Error(w, "Failed to acquire semaphore", http.StatusServiceUnavailable)
        return
    }
    // Освобождаем семафор после выполнения.
    defer sem.Release(1)

    // Логика обработки
    time.Sleep(2 * time.Second)
    w.Write([]byte("Request processed successfully"))
}

Буферизованный канал — простой и идиоматичный способ для базовых случаев. Семафор из x/sync предоставляет больше контроля, например, возможность захвата с разным "весом" и интеграцию с context.

Ответ 18+ 🔞

А, ну ты глянь, какой вопрос подкинули! Ограничить, чтобы не больше N штук одновременно лезло, а не по времени. Это ж элементарно, Ватсон, но народ-то путает, блядь, с rate limiting'ом!

Так, слушай сюда, у меня для тебя два рабочих способа, оба без всяких левых библиотек, на чистом гоёвском коленкоре.

1. Способ для простых пацанов: буферизованный канал, он же семафор

Представь себе очередь в сортир на десять кабинок. Канал — это дверь. Вошёл — занял кабинку (записал в канал), вышел — освободил (прочитал из канала). Если кабинки все заняты — следующий мудак ждёт, пока кто-то не выйдет. Всё, логика кончилась.

// Делаем канал на 10 мест. 10 структур — это 10 наших "разрешений" на вход.
var sem = make(chan struct{}, 10)

func handler(w http.ResponseWriter, r *http.Request) {
    // Пытаемся пролезть в дверь. Если мест нет — тут и зависнем, ждём.
    sem <- struct{}{}
    // А это магическое слово defer — оно как клятва, что мы ВСЕГДА, блядь, освободим кабинку, даже если нас посреди дела прибьёт.
    defer func() { <-sem }()

    // Ну а тут уже твоя бизнес-логика, спи два секунды, например.
    time.Sleep(2 * time.Second)
    w.Write([]byte("Request processed successfully"))
}

Вот и весь сказ, ёпта. Красиво, идиоматично, в две строчки. Хуй с горы, проще некуда.

2. Способ для заумных: официальный семафор (x/sync/semaphore)

Это если тебе мало каналиков, хочется поумничать. Пакет не стандартный, но официальный, с гитхаба golang. Он даёт больше власти, например, можно не по одному "разрешению" захватывать, а пачками (ну, если запрос тяжёлый).

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

// Создаём семафор с общим "весом" в 10 единиц.
var sem = semaphore.NewWeighted(10)

func handler(w http.ResponseWriter, r *http.Request) {
    // Пытаемся урвать одну единичку веса. Используем контекст запроса — если клиент сбежал, то и мы не будем ждать.
    if err := sem.Acquire(r.Context(), 1); err != nil {
        // Не вышло, контекст отменили или ещё какая хрень.
        http.Error(w, "Failed to acquire semaphore", http.StatusServiceUnavailable)
        return
    }
    // И опять наше волшебное defer — страховка от кривых рук.
    defer sem.Release(1)

    // Ну и пошла писать губерния...
    time.Sleep(2 * time.Second)
    w.Write([]byte("Request processed successfully"))
}

Чё выбрать-то? Если задача — "не больше десяти горутин одновременно", то первый способ, канальный, — твой выбор, золотой. Нехуй тут мудрить. Второй — если реально нужны фишки вроде разного "веса" операций или тонкая работа с контекстами. А так-то оба работают, ядрёна вошь!