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

GO КАК ФУНДАМЕНТ Middleware

Middleware в Go

Go

Сквозная логика, цепочка обработчиков, request id, auth, логирование и граница с endpoint.

1. Middleware убирает повторяющуюся обвязку

1 / 5

Логирование, метрики, auth и request id нужны многим маршрутам. Выносите их вместо копирования в каждый handler.

  • Обработчик занимается своим ресурсом.
  • Middleware отвечает за сквозную задачу.
  • Правила группируются по маршрутам.
handler Ahandler Bhandler Cmiddlewarenext

2. Функция оборачивает следующий Handler

2 / 5

Middleware принимает http.Handler и возвращает новый. Перед next.ServeHTTP и после него выполняется своя работа.

  • Можно добавить заголовки или контекст.
  • Можно ответить без вызова next.
  • После next доступны логи и метрики результата.
beforenextafternext.ServeHTTP(w, r)

3. Цепочка имеет порядок

3 / 5

Request id идёт до логов и auth, auth - до бизнес-обработчика, recover - снаружи, чтобы поймать panic остальных.

  • Первым создаётся контекст запроса.
  • Ошибки auth не доходят до handler.
  • Логи получают id, статус и длительность.
request idauthhandlerrecoverПорядок влияет на поведение

4. Request id проходит через контекст

4 / 5

Middleware создаёт идентификатор, кладёт его в context и добавляет в заголовок ответа. Логи и вызовы получают тот же id.

  • Клиент может передать свой id, иначе генерируется новый.
  • Обработчик читает через r.Context().
  • Связь запроса и внешних вызовов сохраняется.
request idcontextX-Request-Idlogs

5. Граница: middleware или handler

5 / 5

Общие технические задачи идут в middleware. Правила конкретного ресурса остаются в handler и домене.

  • Auth, id, логи, метрики, recover - сквозные.
  • Валидация заказа и доступ к нему - предметные.
  • Не прячьте бизнес-логику в обвязку.
middlewareid · logs · authhandler + domainorder · validationГраница ответственности

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

Почти в каждом сервере есть задачи, которые нужно делать для каждого запроса: записать лог, поймать panic, добавить идентификатор запроса. Дублировать этот код в каждом обработчике — плохо. В 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 — ловит panic внутри обработчика и превращает её в ответ 500 Internal Server Error. Без него процесс не упадёт: net/http сам перехватывает panic на уровне соединения, но клиент получит обрыв соединения без ответа, а в логе останется только стандартная строка http: panic serving. Recoverer отвечает нормальным 500 и пишет стек в ваш лог. Не путать с panic в горутине, которую обработчик запустил сам: её никто не ловит, и она роняет весь процесс.

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 кладут только лёгкую логику — он выполняется для каждого запроса. Одна тонкость: http.ResponseWriter не сообщает, какой статус ушёл клиенту. Чтобы записать его в лог, ответ оборачивают — в chi для этого есть middleware.NewWrapResponseWriter, у которого после вызова next доступны Status() и BytesWritten(); именно так устроен готовый middleware.Logger.

Готовые middleware в chi

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

  • RequestID — уникальный идентификатор запроса;
  • Recoverer — перехват panic;
  • 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 кладут только сквозную, общую логику — не бизнес-логику и не разбор конкретных запросов.
  • panic в обработчике без Recoverer это обрыв соединения клиенту, а не падение процесса; panic в своей горутине — падение.

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