Сквозную логику — логи, восстановление после паники, идентификатор запроса — в Go выносят в middleware. И, как всё в Go-вебе, middleware здесь не магия, а простая идея: функция, которая оборачивает один http.Handler в другой. Понять её — значит понять, как в Go устроена вся пересекающая логика.
Middleware — это обёртка
Middleware — функция 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.ServeHTTP выполняется на входе, после — на выходе. Это и есть весь механизм: никаких декораторов, никакой регистрации в контейнере — обычная функция над обычным http.Handler.
Цепочка
Middleware складываются в стек: каждый оборачивает следующий. На chi их вешают через Use, и порядок имеет значение — внешний middleware видит запрос первым и ответ последним.
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Recoverer)
r.Use(Logging)
chi приносит набор готовых middleware (пакет chi/middleware): RequestID, Recoverer, Logger и другие. Своё — пишут как функцию-обёртку и добавляют в ту же цепочку.
Типовые middleware
Три почти всегда нужны на сервисе. RequestID проставляет уникальный идентификатор запроса (в context), по которому потом сшиваются логи и трейсы. Recoverer ловит панику в обработчике и превращает её в 500, не роняя сервер.
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", "err", rec)
http.Error(w, "internal error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
Logging пишет строку на каждый запрос с методом, путём и временем. Тяжёлую логику в middleware не кладут — оно на пути каждого запроса.
Граница: middleware, обработчик, авторизация
Middleware — для сквозного, общего всем маршрутам. Бизнес-логике тут не место; разбор и обработка запроса — в обработчике и Handler-е. Особый случай — авторизация: она тоже middleware (проверка до обработчика), но достаточно объёмная, чтобы вынести её в отдельную статью.
Это тот же приём, что аспекты (AOP) в Spring-биндинге: пересекающую логику отделяют от основной. В Go он сведён к функции-обёртке, и всю цепочку видно в одном месте, при сборке роутера. Идентификатор запроса и логи, проставленные middleware, питают наблюдаемость, без которой продукт-инженер не разберёт инцидент.