← назад к разделу

Почти в каждом сервере есть задачи, которые нужно делать для каждого запроса: записать лог, поймать панику, добавить идентификатор запроса. Дублировать этот код в каждом обработчике — плохо. В Go для таких задач используют middleware.

Проблема: сквозная логика везде

Представьте, что вам нужно логировать каждый входящий запрос. Без middleware приходится писать одно и то же в каждом обработчике:

func handleUsers(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    // ... основная логика ...
    slog.Info("request done", "path", r.URL.Path, "dur", time.Since(start))
}

func handleOrders(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    // ... основная логика ...
    slog.Info("request done", "path", r.URL.Path, "dur", time.Since(start))
}

Middleware решает это: логика выполняется один раз, но для всех запросов.

Как устроен middleware

Middleware в Go — это функция, которая получает обработчик и возвращает новый обработчик. Тип у неё такой: func(http.Handler) http.Handler.

func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r) // вызываем следующий обработчик
        slog.Info("request",
            "method", r.Method,
            "path", r.URL.Path,
            "dur", time.Since(start))
    })
}

Что здесь происходит:

  • next — это тот обработчик, который обрабатывает конкретный маршрут.
  • Возвращаем новый обработчик, который сначала делает своё дело, вызывает next, а потом может ещё что-то сделать.
  • Код до next.ServeHTTP — это «на входе», после — «на выходе».

Никакой магии: просто функция, которая оборачивает другую функцию.

Цепочка middleware

Middleware можно ставить несколько подряд. Каждый оборачивает следующий, и получается стек: внешний видит запрос первым и ответ последним.

На роутере chi это выглядит так:

r := chi.NewRouter()
r.Use(middleware.RequestID) // первым в стеке — RequestID
r.Use(middleware.Recoverer) // потом — Recoverer
r.Use(Logging)              // потом — логирование

Когда приходит запрос, он проходит через RequestID → Recoverer → Logging → ваш обработчик, а ответ идёт в обратном порядке.

Порядок важен. Например, RequestID нужно поставить первым, чтобы все следующие middleware уже имели доступ к идентификатору запроса.

Что обычно кладут в middleware

Три вещи нужны почти на каждом сервере:

RequestID — добавляет уникальный идентификатор к каждому запросу и кладёт его в context. По этому идентификатору потом можно собрать все логи одного запроса вместе — это важно при отладке.

chi поставляет middleware.RequestID из коробки. Если нужно читать идентификатор в обработчике:

requestID := middleware.GetReqID(r.Context())

Recoverer — ловит панику внутри обработчика и превращает её в ответ 500 Internal Server Error. Без него паника уронит весь сервер.

func Recoverer(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                slog.Error("panic recovered", "err", rec)
                http.Error(w, "internal error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

Logging — пишет одну строку на запрос с методом, путём и временем выполнения. В middleware кладут только лёгкую логику — он выполняется для каждого запроса.

Готовые middleware в chi

Пакет github.com/go-chi/chi/v5/middleware содержит набор готовых решений:

  • RequestID — уникальный идентификатор запроса;
  • Recoverer — перехват паники;
  • Logger — стандартное логирование;
  • Compress — сжатие ответа;
  • Timeout — ограничение времени выполнения запроса;
  • RealIP — получение реального IP за прокси.

Своё middleware пишут так же, как показано выше, и добавляют через r.Use.

Где граница: middleware и обработчик

Middleware — для логики, общей всем маршрутам. Бизнес-логика в middleware не живёт.

Обработка конкретного запроса — разбор тела, валидация, ответ — это задача обработчика.

Авторизация — особый случай: она тоже middleware (проверка токена до обработчика), но её достаточно, чтобы вынести в отдельную статью.

Коротко

  • Middleware — функция func(http.Handler) http.Handler, которая оборачивает обработчик и добавляет логику до и/или после него.
  • Несколько middleware образуют цепочку; порядок важен — первый в r.Use видит запрос раньше всех.
  • Три middleware почти всегда нужны: RequestID, Recoverer, Logging.
  • chi поставляет готовый набор в пакете middleware.
  • В middleware кладут только сквозную, общую логику — не бизнес-логику и не разбор конкретных запросов.

Что почитать дальше

  • Роутинг с chi — как настроить маршруты и группы.
  • Context и отмена — как middleware передаёт данные в обработчик через context.
  • Авторизация в Go — middleware для проверки прав доступа.