Сквозную логику — логи, восстановление после паники, идентификатор запроса — в 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, питают наблюдаемость, без которой продукт-инженер не разберёт инцидент.