Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 5 из 25 ~18 мин +50 XP

Middleware в chi: Use и цепочка

Каждый хендлер сервиса рано или поздно обрастает повторами: лог, таймер, проверка токена — один и тот же код в десятке мест. chi middleware решает это обёрткой func(http.Handler) http.Handler: поведение добавляется до и после хендлера, а сами хендлеры остаются чистыми. Разберём сигнатуру, луковицу цепочки, r.Use и цену забытого next.ServeHTTP.

Содержание урока · 10 разделов

В REST-каркасе из прошлого урока хендлеры научились отвечать по делу: карточка заметки, список, обновление. Добавьте к этому нормальный сервис — и каждый хендлер обрастает служебным кодом: замерить время, записать в лог запрос, проверить токен. Три хендлера — три копии одного и того же. Десять — десять, и рано или поздно одну копию правят, а другую забывают. Middleware в chi снимает эти повторы с уровня хендлеров на уровень роутера: поведение описывается один раз как обёртка func(http.Handler) http.Handler и вешается на цепочку одним r.Use. Обёртка сама решает, что сделать до хендлера, что после — и вызвать ли его вообще. Это та же сигнатура, что во всей экосистеме net/http, поэтому сторонние слои подключаются без адаптеров. В этом уроке — механика цепочки, порядок прохождения запроса, ранние ответы и цена забытого вызова next.

Middleware в chi: сигнатура func(http.Handler) http.Handler#

Middleware — функция, которая принимает следующий хендлер и возвращает хендлер. Разберём по частям: func(http.Handler) http.Handler. Аргумент next — всё, что стоит дальше по цепочке: другие middleware и в конце ваш хендлер. Возвращаемое значение — новый http.Handler, обычно замыкание, внутри которого живёт старая цепочка. Внутри замыкания три места для кода. До next.ServeHTTP(w, r) — то, что должно случиться раньше хендлера: замер времени, присвоение id запроса, проверка заголовка. Вызов next.ServeHTTP(w, r) сам — передача эстафеты дальше; без него до маршрутизации и хендлера дело не доходит. После вызова — то, чему нужен результат: длительность, итоговый статус. Приём с defer здесь любимец: код после next гарантированно сработает, даже если хендлер запаниковал. Сигнатура не изобретена chi — это общий язык net/http-экосистемы, и любой middleware из интернета компилируется с chi как есть.

Первый middleware: таймер

GO
func timer(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                start := time.Now()

                defer func() {
                        log.Printf("%s %s: %v", r.Method, r.URL.Path, time.Since(start))
                }()

                next.ServeHTTP(w, r)
        })
}

func main() {
        r := chi.NewRouter()

        r.Use(timer)

        r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
                w.Write([]byte("ok"))
        })

        http.ListenAndServe(":8080", r)
}

Таймер — классический первый middleware. start фиксируется до передачи эстафеты, а log.Printf уходит в defer: строка в логе появится после того, как цепочка отработает, — с методом, путём и длительностью. next.ServeHTTP(w, r) — сердце функции: именно он запускает маршрутизацию и хендлер. r.Use(timer) регистрирует слой на весь роутер. Для запуска нужен локальный Go: песочница сайта TCP-сервер не поднимает.

Луковица: порядок запроса и ответа#

r.Use(timer, logger, auth) складывает слои в стек, и порядок в списке — это направление входа. Запрос идёт снаружи внутрь: сначала timer, потом logger, потом auth, и только затем маршрутизация и хендлер. Ответ возвращается в обратном порядке: auth досмотрел на результат, logger дописал строку, timer посчитал общее время — уже с учётом всех, кто работал внутри. Картинка, которая прилипла к теме, — луковица: каждый middleware — слой, через который запрос проходит туда и обратно. Отсюда два практических вывода. Замер длительности имеет смысл ставить раньше других слоёв — иначе время, потраченное внутренними middleware, в замер не попадёт. А слои, которым нужен результат, например логгер со статусом ответа, располагают так, чтобы их вызов next стоял над тем, кто этот результат меняет; разбору порядка в chi посвящён отдельный урок дальше по курсу. Пока запомните простое: регистрация слева направо — это путь запроса.

Какой будет порядок строк?

Прочитайте код и выберите, что он выведет:

func alpha(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                fmt.Println("alpha: до")
                next.ServeHTTP(w, r)
                fmt.Println("alpha: после")
        })
}

func beta(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                fmt.Println("beta: до")
                next.ServeHTTP(w, r)
                fmt.Println("beta: после")
        })
}

// регистрация: r.Use(alpha, beta); запрос пришёл на существующий маршрут

r.Use: регистрация и правила#

r.Use принимает сколько угодно middleware за один вызов: r.Use(timer, logger) эквивалентен двум строкам r.Use(timer) и r.Use(logger). Слои вешаются на весь роутер; отдельный стек для ветки маршрутов делается через Route и Group — об этом урок через один. И правило, о которое спотыкаются все: Use вызывают до регистрации маршрутов. Chi собирает цепочку один раз — на первом r.Get стек замораживается, и поздний r.Use приводит к панике с говорящим текстом: all middlewares must be defined before routes on a mux. Паника честная: middleware после маршрутов — почти всегда ошибка чтения кода, и chi предпочитает упасть на старте, а не удивлять в проде. Практика сборки: сначала все r.Use сверху вниз, потом маршруты — тогда файл читается как путь запроса, что сверху, то и выполнится раньше.

Три места поведения в middleware#

ФазаЧто доступноПримеры
До next.ServeHTTPзапрос целиком: заголовки, путь, телозамер времени, проверка токена, id запроса
Сам вызов nextэстафета дальше по цепочкеобязателен, если слой не отвечает сам
После next или в deferрезультат: статус, длительность, паникастрока лога, метрика, восстановление

Собираем цепочку из двух слоёв

GO
func main() {
        r := chi.NewRouter()

        // порядок Use = путь запроса
        r.Use(timer)  // снаружи: засекает время первым
        r.Use(logger) // внутри: логирует уже после таймера

        r.Get("/health", health)
        r.Get("/notes/{id}", getNote)

        // GET /notes/42 идёт: timer -> logger -> маршрутизация -> getNote,
        // ответ возвращается в обратном порядке

        http.ListenAndServe(":8080", r)
}

Два слоя плюс хендлер — уже луковица: timer обёрнут вокруг logger, logger вокруг маршрутизации и хендлера. Комментарии в коде — то, что стоит проговаривать при ревью: Use сверху вниз, запрос снаружи внутрь, ответ обратно. Запустите локальным Go и посмотрите в лог: одна строка таймера на каждый запрос, каким бы ни был маршрут — включая 404, о них следующий блок.

Middleware видит и 404#

Запрос на несуществующий путь всё равно проходит через весь зарегистрированный стек. Chi сначала прогоняет запрос через middleware и только потом ищет маршрут; когда ни один шаблон не совпал, в работу включается обработчик 404 — но уже внутри цепочки. Для логов это ключевой сценарий: кривые запросы сканеров, опечатки после редизайна, пробы чужих API — всё попадает в журнал с длительностью и id, как обычные запросы. В фреймворках с другим устройством 404 нередко отдаётся до middleware, и лог молчит ровно тогда, когда интереснее всего. Побочный эффект стоит запомнить: тяжёлые слои считаются и на мусорных запросах, так что middleware, который на каждом входе лезет в базу, начнёт есть ресурсы и на переборе адресов. Служебные проверки делайте дешёвыми и ранними — или ставьте дешёвый фильтр первым слоем.

Проверка: 404 и цепочка

Запрос пришёл на путь, которого нет ни в одном маршруте. Что произойдёт с зарегистрированными middleware?

Ранний ответ: middleware отвечает сам#

Вызов next — не обязанность любой ценой, а выбор. Если дальше идти нельзя, middleware отвечает сам и эстафету не передаёт: проверка токена не сошлась — w.WriteHeader(http.StatusUnauthorized) и return; квота исчерпана — 429. Клиент получает ответ, остальные слои и хендлер не тратят ни такта. Заметьте: «после» в слоях выше всё равно сработает — defer в таймере напечатает строку и для отклонённого запроса, ведь ответ вернулся по цепочке, просто не дошёл до хендлера. Обратная задача — передать данные вниз, в хендлер: в Go это делается через context.Context, к которому доступ из *http.Request. Разбор контекста — в уроке продвинутого курса, а собственный middleware с пользователем в контексте будет позже в курсе chi, после знакомства с готовыми слоями.

Middleware с ранним ответом

GO
func auth(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                if r.Header.Get("Authorization") == "" {
                        http.Error(w, "нет токена", http.StatusUnauthorized)
                        return // эстафета не передаётся
                }

                next.ServeHTTP(w, r)
        })
}

r := chi.NewRouter()
r.Use(auth)
r.Get("/notes", listNotes)

Две ветки: без заголовка Authorization слой сам отвечает 401 Unauthorized через http.Error и возвращает управление — хендлер listNotes не выполнится никогда для такого запроса. С заголовком — next.ServeHTTP(w, r), и запрос уходит дальше. Ранний ответ — штатная функция middleware, а не обходное правило; настоящую проверку токена и передачу пользователя в контексте соберём позже. Здесь — упрощение ради механики.

Проверка: пропущенный next

Middleware не вызвал next.ServeHTTP и не записал ничего в http.ResponseWriter. Что получит клиент?

Соберите таймер

Восстановите замер времени, строку лога в defer и передачу эстафеты.

func timer(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time. defer func() { log.Printf("%s %s: %v", r.Method, r.URL.Path, time.(start)) }() next.(w, r) }) }

Пять правил первого middleware#

  • Одна ответственность на слой: таймер меряет, логгер пишет, авторизация пускает — слияние трёх задач ломает переиспользование.
  • Либо next.ServeHTTP, либо собственный ответ — третьего варианта нет; пропущенный вызов оставляет клиента без ответа.
  • Код после next выносите в defer: он сработает и на панике хендлера.
  • r.Use — до регистрации маршрутов: поздний Use в chi приводит к панике на старте.
  • Порядок Use = путь запроса: тот, кто должен посчитать всех, — первым.

Соберите таймер по строкам

Расставьте строки middleware-таймера в правильном порядке: от сигнатуры до передачи эстафеты.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: цепочка middleware

Термин

Middleware

нажмите, чтобы перевернуть
Определение

func(http.Handler) http.Handler — обёртка вокруг следующего хендлера: добавляет поведение до, после или вместо него. Та же сигнатура во всей экосистеме net/http.

1 / 6

Механика собрана: сигнатура, луковица, правила r.Use, ранние ответы и 404, которые не теряются из логов. Писать каждый слой руками, впрочем, долго — большая часть обвязки уже написана авторами chi. В следующем уроке — встроенные middleware: RequestID, RealIP, Logger и Recoverer, из которых боевой стек собирается четырьмя строками r.Use. К собственному middleware с авторизацией и контекстом вернёмся после знакомства с готовыми слоями, а группы маршрутов со своим стеком — тема урока про Route и Group, через один. Попробуйте напоследок поменять местами r.Use(timer) и r.Use(logger): разница между строками двух слоёв покажет на живом коде, кто кого оборачивает.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Middleware — func(http.Handler) http.Handler: обёртка принимает следующий хендлер и возвращает новый; поведение живёт до, после или вместо next.ServeHTTP.
  • Луковица: запрос проходит слои в порядке регистрации, ответ — в обратном; код после next выносится в defer.
  • r.Use(timer, logger) регистрирует стек на весь роутер и вызывается до маршрутов — поздний Use в chi приводит к панике на старте.
  • next.ServeHTTP обязателен: без него и без собственного ответа клиент висит до таймаута — маршрутизация не запускается.
  • Middleware может ответить сам — 401, 429 — и не вызывать next; внешние слои всё равно досчитают своё «после».
  • 404 проходит через весь стек: логи, id и таймеры видят и мусорные запросы — вместе с их ценой.
  • Бизнес-логика в цепочке — редкая удача, а не правило: middleware держат для инфраструктуры и проверок безопасности.

Домашнее задание

Напишите к каркасу заметок два middleware: timer с defer и log.Printf, и headerCheck, отвечающий 401 без заголовка X-Role. Зарегистрируйте их r.Use до маршрутов и проверьте curl-ом три ответа: обычный запрос, запрос без роли и путь к несуществующему маршруту — таймер должен отработать во всех трёх.

Личные заметкиПусто
Хранятся только в вашем браузере (Chi · Middleware в chi: Use и цепочка)

Вопросы и ответы по уроку

Чем middleware в chi отличается от middleware в Gin?

Сигнатурой и типами: chi — func(http.Handler) http.Handler со стандартными w и r, Gin — func(*gin.Context), и свойство слоя завязано на gin-контекст. Механика луковицы одинакова, но chi-слой компилируется с любым net/http-кодом, а gin-слой живёт только внутри Gin.

Что произойдёт, если вызвать r.Use после регистрации маршрутов?

Chi запаникует на старте с текстом all middlewares must be defined before routes on a mux: стек замораживается на первом r.Get. Это fail-fast вместо сюрпризов в проде — собирайте r.Use до маршрутов, тогда файл читается как путь запроса.

Можно ли поставить middleware только на часть маршрутов?

На весь роутер — r.Use, на ветку — Route или Group со своим стеком, о них отдельный урок дальше; на одиночный маршрут слой вешается вручную оборачиванием хендлера. Так авторизация живёт в ветке /admin, а логи остаются общими для всего сервиса.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Redis25 уроков~12 чпрактика25 уроков уже доступны

Redis в Go: кэш, сессии и очереди на go-redis

Курс Redis для Go-разработчиков: кэширование запросов, сессии, счётчики, Pub/Sub, Streams и rate limiter. 25 уроков с примерами кода go-redis v9.

  • Подключаться к Redis из Go и настраивать пул соединений
  • Кэшировать результаты запросов и придумывать TTL
  • Работать с хэшами, списками, множествами и ZSET
  • Строить сессии, счётчики, лидерборды и rate limiter