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

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

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

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

Встроенные middleware: Logger, Recoverer, RequestID

Встроенные middleware chi из пакета chi/middleware закрывают базовую обвязку сервиса: RequestID присваивает каждому запросу идентификатор, RealIP выправляет адрес клиента за прокси, Logger пишет строку на запрос, Recoverer ловит панику со стеком. Разберём каждый слой, честные ловушки и порядок установки боевого стека из четырёх middleware.

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

Сервис из прошлого урока умеет оборачивать запросы своими слоями. Два события проверяют его на прочность: в логах копятся строки без общего знака — какой запрос из них чей, — и паника в одном хендлере роняет весь процесс вместе с остальными живыми запросами. Обе проблемы решает пакет chi/middleware: встроенные middleware chi — RequestID, RealIP, Logger, Recoverer и ещё десяток утилит — собраны авторами библиотеки и подключаются одной строкой r.Use каждая. Импорт — github.com/go-chi/chi/v5/middleware: подпакет живёт рядом с корневым, и имя пакета middleware в коде читается почти как список покупок. Здесь разберём четыре главных слоя, коротко — Timeout, Heartbeat и NoCache, а в конце соберём боевой стек из четырёх middleware и поговорим о порядке установки, который chi фиксирует прямо в исходниках.

RequestID: метка каждого запроса#

middleware.RequestID достаёт идентификатор из заголовка X-Request-Id, а если его нет — генерирует свой: префикс с именем хоста и случайной строкой плюс счётчик запросов. Идентификатор кладётся в контекст запроса, откуда его достаёт middleware.GetReqID(r.Context()) — из любого middleware ниже по цепочке. Зачем это нужно: одна пользовательская операция в распределённом сервисе порождает запросы к нескольким службам, и общий id — единственная ниточка, по которой журнал одной службы сшивается с журналом другой. Прокладка простая: балансировщик генерирует X-Request-Id, chi на входе его переиспользует, ваши слои логируют тот же id. Когда что-то сломалось, поддержка первым делом запрашивает идентификатор запроса — и вторым делом советует печатать его в ответах об ошибках. Logger и Recoverer из этого пакета подхватывают id автоматически — но только если RequestID стоит в стеке раньше них; к порядку вернёмся после знакомства со слоями.

RealIP: адрес клиента за прокси#

За nginx или HAProxy все запросы приходят с адресом прокси: r.RemoteAddr показывает 127.0.0.1, и лимиты по IP, гео-фильтры и статистика работают с одним и тем же «клиентом». middleware.RealIP смотрит на заголовки True-Client-IP, X-Real-IP и X-Forwarded-For — в этом порядке — и подменяет r.RemoteAddr на найденный адрес, после чего вся цепочка видит реального клиента. У слоя тяжёлая оговорка, которую chi теперь пишет прямо в коде: middleware помечен как deprecated. Причина — подмена адреса: заголовки слепо верят отправителю, и клиент без прокси может выдать себе любой X-Forwarded-For. Свежие версии chi предлагают семейство ClientIPFrom* с явным списком доверенных прокси и чтение адреса через GetClientIP. Для действующих проектов с nginx спереди RealIP остаётся рабочим, но правило неизменно: слой ставится только там, где перед сервисом стоит прокси, которому вы доверяете.

Боевой стек из четырёх middleware

GO
package main

import (
        "net/http"

        "github.com/go-chi/chi/v5"
        "github.com/go-chi/chi/v5/middleware"
)

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

        r.Use(middleware.RequestID)
        r.Use(middleware.RealIP)
        r.Use(middleware.Logger)
        r.Use(middleware.Recoverer)

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

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

Боевой минимум из документации chi. RequestID раньше Logger — тогда каждая строка лога несёт [id]; RealIP до Logger — в логе адрес клиента, а не прокси. Logger раньше Recoverer — chi прямо документирует это в комментарии к Logger: 500 от Recoverer проходит через обёртку ответа Logger, и строка лога показывает честный статус. Проверьте стек локальным Go — песочница сайт сеть не поднимает.

Обзор встроенных middleware chi#

MiddlewareЧто делаетЧто будет без него
RequestIDid из X-Request-Id или генерация, в контекст запросалоги служб не сшиваются между собой
RealIPTrue-Client-IP, X-Real-IP, X-Forwarded-For в r.RemoteAddrза прокси все клиенты выглядят как прокси; в новых chi deprecated
Loggerтекстовая строка на запрос: метод, адрес, статус, байты, времяв логе пусто, пока не написан свой слой
Recovererловит панику, пишет стек в лог, отдаёт 500паника одного хендлера роняет процесс
Timeoutдедлайн в контексте запроса, после него 504зависший хендлер держит соединение бесконечно
Heartbeatкрошечный ответ на заданный путь вроде /pingбалансировщик проверяет здоровье через боевые маршруты
NoCacheзаголовки против кэширования, чистка ETagпрокси и браузеры кэшируют по своему усмотрению

Logger: строка на каждый запрос#

middleware.Logger печатает строку на каждый завершённый запрос: время, [request id], метод и адрес в кавычках, источник — from, затем статус, размер ответа в байтах и длительность; в терминале статус покрашен. Строка собирается после выполнения цепочки: Logger оборачивает ResponseWriter, запоминает статус и размер, а печатает всё разом в defer — поэтому в лог попадают и ранние ответы из middleware, и 500 от Recoverer. Для разработки это идеальный инструмент: включил — и видишь всё. В продакшене текстовых строк быстро перестаёт хватать: их нельзя фильтровать по уровню и полям, а поиск по десяткам сервисов превращается в grep-марафон. Стандартное движение — структурный логгер: в курсе zap этому посвящён урок про HTTP-middleware, где chi-стек соединяется с zap. Logger остаётся для консоли и стендов, структурный логгер — для централизованных журналов.

Recoverer: паника не роняет процесс#

Паника в Go не перехватывается между горутинами: то, что упало внутри хендлера, убивает весь процесс — вместе с сотнями обрабатываемых запросов. middleware.Recoverer разрывает сценарий: в defer слой ловит recover(), печатает стек в журнал и отдаёт клиенту 500 Internal Server Error. С Logger в стеке стек-трейс попадает в общий журнал запросов, без него — на stderr в виде раскрашенного стека. Клиент получает честный 500, а не обрыв соединения, и мониторинг видит ошибку в статус-кодах. Деталь из исходников: панику http.ErrAbortHandler Recoverer сознательно не перехватывает — это сигнал net/http «оборвать соединение», и мешать ему нельзя. Recoverer — предохранитель, а не исправление: данные могли быть записаны наполовину, транзакция брошена. Правильная реакция на 500 из паники — починить код, который её вызвал, а не радоваться живучести.

Проверка: паника под Recoverer

В стеке RequestID, RealIP, Logger, Recoverer. Хендлер вызвал panic("boom"). Что получит клиент?

Что покажет строка лога?

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

r := chi.NewRouter()

r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)

r.Get("/boom", func(w http.ResponseWriter, r *http.Request) {
        panic("boom")
})

// запрос: GET /boom

Heartbeat и NoCache: по месту

GO
r := chi.NewRouter()

r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)

// health-check: балансировщик стучится в /ping
r.Use(middleware.Heartbeat("/ping"))

// динамическое API не должно кэшироваться
r.Use(middleware.NoCache)

Heartbeat("/ping") отвечает точкой и 200 на GET и HEAD по заданному пути — маршруты и авторизации даже не затрагиваются, поэтому слой ставят первым в стеке. NoCache расставляет заголовки no-cache и no-store и вычищает ETag — для динамических API, где кэшировать имеет право только явный Cache-Control. Утилит в пакете шире: Throttle с очередью и Compress с gzip — у каждого свой урок дальше по курсу.

Timeout и дедлайн в контексте

GO
r := chi.NewRouter()

r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(30 * time.Second))

r.Get("/report", func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        select {
        case <-ctx.Done():
                // дедлайн истёк — выходим, 504 напишет Timeout
                return
        case <-time.After(10 * time.Second):
                w.Write([]byte("готово"))
        }
})

Timeout(30 time.Second) кладёт дедлайн в контекст запроса: по истечении клиенту пишется 504 Gateway Timeout — но хендлер сам не прервётся, он обязан следить за r.Context().Done(), иначе сигнал уйдёт впустую. Честный контракт, а не кнопка «убить зависший код». В примере работа на десять секунд помещается в дедлайн тридцать — хендлер отвечает «готово»; поставьте Timeout(5 time.Second) — и запрос завершится 504.

Порядок стека: почему Logger раньше Recoverer#

Порядок r.Use(a, b) — это путь запроса из прошлого урока, и для боевого стека chi фиксирует рекомендации в исходниках. RequestID первым: идентификатор должен лежать в контексте до того, как Logger соберёт строку, — тогда в каждой записи есть [id]. RealIP до Logger: иначе в логе адрес прокси, а не клиента. Logger раньше Recoverer — контринтуитивно для привыкших, что recovery ставят первым слоем, но у chi расчёт на то: Recoverer, поймав панику, пишет 500 в обёрнутый Logger-ом ResponseWriter, и строка лога показывает 500, а сам стек паники уходит в общий журнал через запись, которую Logger положил в контекст. Поменяйте местами — клиент всё равно получит 500, но строка лога покажет 200: обёртка не видела записи статуса. Recoverer замыкает стек — ближе всего к хендлерам, где паники и рождаются.

Проверка: место RealIP

Реальный адрес клиента нужен в строках лога. Где место middleware.RealIP?

Соберите боевой стек

Восстановите три слоя стека в порядке, который рекомендует chi: идентификатор, лог, защита от паники.

r := chi.NewRouter() // идентификатор запроса — раньше всех r.Use(middleware.) // строка лога с [id] и статусом r.Use(middleware.) // паника не уронит процесс r.Use(middleware.)

Минимальный боевой стек — и что добавить следом#

  • r.Use(middleware.RequestID) — id каждого запроса; переиспользует X-Request-Id от балансировщика.
  • r.Use(middleware.RealIP) — адрес клиента за прокси; без доверенного прокси слой не включается.
  • r.Use(middleware.Logger) — строка на запрос с id, статусом и длительностью; в продакшене — структурный логгер.
  • r.Use(middleware.Recoverer) — паника превращается в 500 со стеком в журнале.
  • Следом по надобности: Timeout(30 * time.Second) и Throttle(100) — дедлайны и предохранитель от перегрузки.
  • Для health-check балансировщика — Heartbeat("/ping") первым слоем; для API без кэша — NoCache.

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

Расставьте четыре слоя боевого стека в порядке, который рекомендует chi: от самого внешнего слоя к самому внутреннему.

Строки

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

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

Карточки: встроенные middleware

Термин

middleware.RequestID

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

Берёт X-Request-Id из заголовка или генерирует id (хост, случайный префикс, счётчик) и кладёт в контекст; достаётся middleware.GetReqID(r.Context()).

1 / 6

Стек собран: четыре строки r.Use — и сервис умеет называть запросы, видеть клиента за прокси, вести журнал и переживать панику. Дальше по курсу слои станут адресными: Route и Group дают веткам маршрутов собственные стеки — авторизация только для /admin, лимиты только для API, — а общий боевой каркас остаётся на весь роутер. Полная карта курса — на странице chi: от JSON API и централизованных ошибок до graceful shutdown и деплоя за прокси. Осталась одна честная оговорка урока: стек проверяется локальным Go — curl-ом на /ping и с нарочно запаниковавшим /boom, чтобы увидеть 500 и стек в журнале своими глазами.

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

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

Вывод

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

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

  • Пакет chi/middleware подключается как github.com/go-chi/chi/v5/middleware; каждый слой включается одной строкой r.Use.
  • RequestID берёт X-Request-Id или генерирует id и кладёт его в контекст; GetReqID достаёт его из любого слоя ниже по цепочке.
  • RealIP переписывает RemoteAddr по True-Client-IP, X-Real-IP и X-Forwarded-For — только за доверенным прокси; в новых chi слой deprecated в пользу ClientIPFrom*.
  • Logger печатает строку с [id], методом, адресом, статусом, размером и длительностью; для продакшена — структурный логгер вроде zap.
  • Recoverer ловит панику, пишет стек в журнал, возвращает 500 и сознательно пропускает http.ErrAbortHandler; горутины хендлера он не защищает.
  • Timeout кладёт дедлайн в контекст и пишет 504, Heartbeat отвечает точкой на /ping, NoCache чистит кэширующие заголовки.
  • Боевой порядок: RequestID, RealIP, Logger, Recoverer — Logger раньше Recoverer, чтобы строка лога показала 500, а паника ушла в общий журнал.

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

Соберите боевой стек из урока в каркасе заметок: RequestID, RealIP, Logger, Recoverer. Добавьте маршрут /boom с panic, Heartbeat("/ping") и Timeout(30 * time.Second). Запустите локальным Go, вызовите /boom и убедитесь, что клиент получил 500, строка лога показывает 500, а стек паники виден в журнале.

Личные заметкиПусто
Хранятся только в вашем браузере (Chi · Встроенные middleware: Logger, Recoverer, RequestID)

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

Чем middleware.Logger из chi отличается от zap?

Logger печатает текстовую строку на каждый запрос в stdout — без уровней и структуры: удобно в разработке и на стендах. Zap пишет структурные события с уровнями и полями, которые фильтруются и собираются централизованно. В продакшене строковый Logger меняют на структурный логгер.

Нужно ли ставить все встроенные middleware?

Нет: стек собирается под сервис. RequestID и Recoverer оправданы почти всегда, Logger — почти везде, RealIP — только за доверенным прокси. Timeout, Throttle, NoCache и Heartbeat добавляются по мере появления дедлайнов, нагрузки и health-check — по одному слою с пониманием его работы.

Что получит клиент при панике, если Recoverer не в стеке?

Соединение оборвётся без ответа: net/http напишет сообщение о панике в свой журнал и закроет соединение, а процесс вместе с активными запросами погибнет. Recoverer держит 500 и живой процесс, но это предохранитель — причину паники чинит разработчик, а не middleware.

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

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

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

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

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

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

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