Почти в каждом сервере есть задачи, которые нужно делать для каждого запроса: записать лог, поймать панику, добавить идентификатор запроса. Дублировать этот код в каждом обработчике — плохо. В 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 для проверки прав доступа.