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

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

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

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

Middleware: понятие и встроенные

Тема «fiber middleware» — это механизм, на котором держится каждый сервер Fiber: логи, таймеры, идентификаторы запросов и защита от паник не пишут внутри хендлеров, а вешают слоями на общую цепочку. Разберём сигнатуру Handler по исходникам v3, механику c.Next() и луковицы, прерывание цепочки ошибкой, а затем три встроенных middleware — Logger, Recover и RequestID — с их конфигами, тегами и граблями порядка регистрации.

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

fiber middleware: слой вокруг каждого обработчика#

Middleware — это функция того же типа, что и обычный хендлер, которую Fiber исполняет не ради ответа, а вокруг ответов: логирует, ставит заголовки, проверяет доступ, ловит паники. Вы уже встречались с этой механикой, не называя её: в уроке 5 статик-хендлер при промахе мимо файла вызывал c.Next() — так запрос проваливался дальше по цепочке, к API и SPA-fallback. Статика в Fiber и есть middleware, только с файловой системой внутри.

Смысл подхода — один раз описать сквозную заботу и больше не повторять её в хендлерах. Лог строки запроса нужен каждому маршруту: если написать его в каждом хендлере, к третьему эндпоинту вы будете копировать одно и то же; если повесить middleware — получите логи даже для тех маршрутов, которые добавите через месяц. Продолжаем сквозной пример курса — API заметок: к нему прицепим три встроенных слоя, которые идут в комплекте с фреймворком. Все факты урока сверены с исходниками тега v3.5.0 — файла app.go и каталога middleware.

Сигнатура Handler: middleware — это обычный хендлер

GO
// app.go, строка 41 (v3.5.0): тип, из которого состоит всё приложение
type Handler = func(Ctx) error

// Минимальный middleware: замеряем время вокруг всей цепочки
func timer(c fiber.Ctx) error {
        // фаза запроса: код до c.Next() исполняется первым
        start := time.Now()

        // эстафета следующим слоям — вплоть до хендлера
        err := c.Next()

        // фаза ответа: сюда попадаем, когда внутренние слои закончили
        fmt.Printf("%s %s -> %v\n", c.Method(), c.Path(), time.Since(start))
        return err
}

// Подключение: все методы, все пути
app.Use(timer)

Главная мысль урока помещается в одну строчку: в Fiber нет отдельного типа для middleware. В app.go объявлен type Handler = func(Ctx) error, и этим типом описаны и хендлеры, и слои вокруг них — различает их только способ подключения. Обратите внимание на Ctx без звёздочки: в v3 контекст — интерфейс (за ним стоит DefaultCtx), а в v2 сигнатура была func(fiber.Ctx) error с указателем; старые туториалы выдают себя именно звёздочкой. Метод Use принимает variadic any: строку-префикс, срез строк, вложенное приложение App или любое число хендлеров — на незнакомый тип он честно паникует с «use: invalid handler».

c.Next() и луковица: две фазы жизни слоя#

Как цепочка работает внутри: каждый сматченный маршрут несёт массив хендлеров, а Use-слои встают в общий стек раньше endpoint-маршрутов. Метод c.Next() (в ctx.go документирован как «executes the next method in the stack») двигает индекс и вызывает следующий слой; когда те закончили, управление возвращается в строку после вызова. Отсюда модель, которую все называют луковицей: запрос идёт снаружи внутрь сквозь слои, хендлер сидит в центре, ответ выходит обратно через те же слои. У каждого middleware поэтому две фазы: код до c.Next() видит входящий запрос, код после — уже готовый ответ с финальным статусом.

Практическое следствие: замерять время и читать c.Response().StatusCode() имеет смысл только после c.Next() — до него ответ ещё не существует. Именно так устроен штатный таймер в Logger-е. И отсюда же правило порядка: слои, зарегистрированные раньше, оборачивают остальные; слой, поставленный после endpoint-маршрута, почти никогда не получит управления — хендлер, не вызвавший c.Next(), завершает обработку сам. Мы ещё вернёмся к порядку при разборе связки Logger и Recover — там цена перестановки конкретная: потерянная строка лога.

В каком порядке уйдут строки в консоль?

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

func first(c fiber.Ctx) error {
        fmt.Println("A: до")
        err := c.Next()
        fmt.Println("A: после")
        return err
}

func second(c fiber.Ctx) error {
        fmt.Println("B: до")
        err := c.Next()
        fmt.Println("B: после")
        return err
}

app.Use(first, second)
app.Get("/ping", func(c fiber.Ctx) error {
        fmt.Println("H: обработчик")
        return c.SendString("pong")
})

Прерывание цепочки: return err до c.Next()#

Middleware не обязан дотягивать запрос до хендлера. Достаточно вернуть ошибку, не вызвав c.Next(), — внутренние слои не исполнятся вовсе, а ошибка уйдёт в глобальный обработчик. В докомментарии конфига это сформулировано прямо: ErrorHandler is executed when an error is returned from fiber.Handler. Что делает обработчик по умолчанию (DefaultErrorHandler в app.go): если ошибка — или заворачивает в себе — *fiber.Error, клиент получает её код; любая другая ошибка превращается в 500 с телом err.Error(); Content-Type при этом text/plain; charset=utf-8. Значит, статус ответа определяет не middleware, а тип ошибки, которую вы вернули.

Второй способ прервать цепочку — вернуть nil без c.Next(): никто не упадёт, но и ответ никто не соберёт. Клиент получит то, что успели записать до возврата, — как правило, пустой 200. Это почти всегда баг, а не приём, и ниже мы отведём ему отдельный предупреждающий блок. Тонкость про луковицу: когда слой вернул ошибку до Next, код после c.Next() у вышележащих слоёв всё равно исполнится — но их c.Next() вернёт эту самую ошибку, и они обязаны просто прокинуть её дальше, ничего не перезаписывая. Так ошибка всплывает наверх, а ErrorHandler получает её один раз.

Отказ до хендлера: fiber.NewError и аргументы маршрута

GO
// Фабрика middleware: конфигурация снаружи, готовый Handler на выходе
func requireKey(key string) fiber.Handler {
        return func(c fiber.Ctx) error {
                // фаза запроса: проверяем заголовок до всей работы
                if c.Get("X-Api-Key") != key {
                        // прерываем цепочку: код 401 придёт клиенту как есть
                        return fiber.NewError(fiber.StatusUnauthorized, "неверный ключ")
                }
                // ключ подошёл — пускаем дальше
                return c.Next()
        }
}

// Подключение аргументом маршрута: слой только для этого пути
app.Get("/api/notes", requireKey("s3cret"), func(c fiber.Ctx) error {
        return c.JSON(fiber.Map{"items": []string{"первая заметка"}})
})

Три детали. Первая: fiber.NewError(code, message) возвращает *fiber.Error — именно такой ошибки ждёт DefaultErrorHandler, поэтому клиент получит 401 и текст «неверный ключ», а не обезличенный 500; полноценную аутентификацию-заглушку мы построим сами в седьмом уроке, здесь важна механика прерывания. Вторая: middleware можно вешать не только через Use, но и аргументами маршрута — они встают в массив route.Handlers перед хендлером, и c.Next() перебирает их внутри этого же маршрута. Третья: фабрика, возвращающая fiber.Handler, — тот же рисунок, что у logger.New() и requestid.New(): конфиг на входе, слой на выходе.

Проверьте себя

Middleware для /api/notes вернул fiber.NewError(fiber.StatusNotFound, "нет такой заметки"), не вызвав c.Next(). Что получит клиент?

Куда вешать middleware: пять способов подключения#

СпособПримерКто получит слой
Use без префиксаapp.Use(logger.New())Все запросы, зарегистрированные после него
Use с префиксомapp.Use("/api", limiter)Только пути под /api, любой HTTP-метод
Аргументы маршрутаapp.Get("/notes", mw, handler)Только этот маршрут: слои в route.Handlers
Монтирование приложенияapp.Use("/api", subApp)Подпрефиксное приложение со своей конфигурацией
Регистрация после маршрутовapp.Get(...); затем app.Use(mw)Слой в стеке позже: живые хендлеры его не дождутся
Next в конфиге middlewarelogger.Config{Next: fn}Слой пропускается, когда Next вернул true

fiber logger: формат и теги-переменные#

Первый встроенный слой — access-log. Пакет github.com/gofiber/fiber/v3/middleware/logger экспортирует logger.New(config ...Config), который возвращает готовый fiber.Handler: внутри себя он вызывает c.Next(), а потом печатает строку — потому и видит финальный статус. Формат задаётся строкой с тегами-переменными вида ${status}: дефолт объявлен в format.go константой DefaultFormat — «[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}» с переводом строки. Теги собираются из tags.go: время, ${ip}, ${status}, ${latency}, ${method}, ${path}, ${url}, ${route}, ${ua}, ${referer}, ${protocol}, ${bytesSent}, ${error}, плюс параметризованные ${reqHeader:X}, ${respHeader:X}, ${locals:X}, ${query:X}, ${cookie:X} и цвета для терминала. Свои теги регистрируются функцией RegisterTag или MustRegisterTag — глобально на имя.

Про «fiber logger» полезно знать три вещи сверх тегов. Во-первых, есть готовые форматы под задачи: CommonFormat и CombinedFormat — классические Apache-формы для парсеров, JSONFormat и ECSFormat — для коллекторов логов. Во-вторых, конфиг богаче, чем кажется: Stream (по умолчанию os.Stdout), TimeFormat «15:04:05», TimeZone «Local», TimeInterval в полсекунды, DisableColors. В-третьих, в дефолтном конфиге строка выглядит не как шаблон, а «столбиками» — время | статус | латентность | IP | метод и путь | ошибка: для DefaultFormat есть быстрый путь рендера. Значения из запроса перед печатью чистятся от управляющих символов — клиент с хитрым путём не подделает строки вашего лога.

logger.New: дефолт и свой формат

GO
import "github.com/gofiber/fiber/v3/middleware/logger"

// Вариант 1: нулевая настройка — дефолтный DefaultFormat
app.Use(logger.New())

// Вариант 2 (вместо первого): свой формат из тегов
app.Use(logger.New(logger.Config{
        Format:     "${time} | ${status} | ${latency} | ${method} ${path} | ${error}\n",
        TimeFormat: "15:04:05",
}))

На каждый запрос при втором варианте уйдёт строка вроде «14:07:33 | 200 | 1.4ms | GET /api/notes | ». Тег ${error} пуст, если цепочка вернула nil, и содержит текст ошибки иначе — сейчас это важно, ниже разберём почему. Конфиг передаётся первым аргументом-вариадиком: незаполненные поля добираются из ConfigDefault, поэтому указывать одно поле Format легально. Два Use с логгером подряд в реальном приложении не нужны — выберите один вариант, здесь они показаны рядом для сравнения.

Ошибка в цепочке: что будет в логе и в ответе?

Логгер зарегистрирован с тегом ${error}. Внутренний слой вернул fiber.NewError(fiber.StatusForbidden, "нет доступа"). Что произойдёт?

fiber Recover: паника вместо падения процесса#

Второй слой отвечает на вопрос «что будет, если хендлер запаниковал». Ответ без middleware неприятный: fasthttp не восстанавливает паники — в server.go это зафиксировано комментарием «no panic recovery is done by fasthttp, thus any panic will take down the entire server». Одна забытая проверка на nil — и весь ваш сервис умирает вместе с обслуживанием других запросов. Для сравнения: стандартный net/http ловит панику хендлера на уровне сервера и продолжает жить; Fiber же предлагает решать вопрос явно — middleware-ем recover из пакета .../middleware/recover.

Механика элементарная и стоит того, чтобы вы её поняли: хендлер recover-слоя объявлен с именованным возвратом err, и внутри стоит defer с recover(). Паника из любого внутреннего слоя разматывает стек ровно до этого defer-а, её значение превращается в ошибку — err = PanicHandler(c, r) — и цепочка завершается штатным возвратом ошибки. Паника по умолчанию (DefaultPanicHandler) становится error как есть, если паниковали значением-ошибкой, иначе оборачивается через fmt.Errorf; значит, паника с *fiber.Error сохранит свой код, а паника со строкой «boom» даст 500. Стек вызовов по умолчанию не печатается: EnableStackTrace false, но при включении StackTraceHandler пишет в stderr строку «recovered panic: …» и полный debug.Stack() — для отладки удобно, в проде шумно.

recover.New: две настройки

GO
import "github.com/gofiber/fiber/v3/middleware/recover"

// Базовая защита: паника превращается в error -> ErrorHandler -> 500
app.Use(recover.New())

// Отладочный вариант: стек паники уйдёт в stderr
app.Use(recover.New(recover.Config{
        EnableStackTrace: true,
}))

Конфиг невелик: Next (пропустить слой целиком), PanicHandler (как превратить значение паники в error), StackTraceHandler и флаг EnableStackTrace. Достаточно одного экземпляра на приложение — defer с recover() ловит паники из всех слоёв, зарегистрированных после него. Импорт пакета выглядит непривычно: пакет называется recover, как встроенная функция, — авторы пометили это nolint-комментарием с пометкой о переименовании, но пока пишется именно так.

Что получит клиент вместо ответа?

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

app.Use(recover.New())

app.Get("/boom", func(c fiber.Ctx) error {
        panic("boom")
})

Сервер без recover-слоя

В приложении НЕТ middleware recover. Один из хендлеров паникует на запросе. Что произойдёт с сервером?

fiber RequestID: X-Request-ID для трассировки#

Третий встроенный слой решает задачу, которую вы оцените при первом же инциденте: дать каждому запросу идентификатор, который виден в логах, в ответах и в системах трассировки. Пакет .../middleware/requestid с фабрикой requestid.New() работает так: читает входящий заголовок (по умолчанию X-Request-ID — константа fiber.HeaderXRequestID), и если значение валидно — непусто и состоит из видимых ASCII-символов, — переиспользует его; иначе генерирует новое через конфигурируемый Generator (дефолт — utils.SecureToken), с тремя попытками и запасным вариантом на случай капризного генератора. Готовый ID записывается в тот же заголовок ответа через c.Set и складывается в контекст запроса, откуда его достаёт requestid.FromContext.

Переиспользование входящего ID — не мелочь: клиент или балансировщик может прислать свой идентификатор, и тогда он пройдёт сквозь ваш сервис без изменений — сквозная трассировка собирается бесплатно. А связка с логгером тут буквальная: при первом запуске requestid-middleware регистрирует в logger теги ${requestid} и ${request-id}, после чего их можно вставлять прямо в формат access-log. Два независимых пакета согласованы внутри одной библиотеки — вам не нужно протаскивать ID руками через locals. Проверка простая: curl -i, и в ответе появится строка X-Request-ID со случайным токеном.

Собираем три слоя в main.go заметок

GO
package main

import (
        "log"

        "github.com/gofiber/fiber/v3"
        "github.com/gofiber/fiber/v3/middleware/logger"
        "github.com/gofiber/fiber/v3/middleware/recover"
        "github.com/gofiber/fiber/v3/middleware/requestid"
        "github.com/gofiber/fiber/v3/middleware/static"
)

func main() {
        app := fiber.New()

        // 1. RequestID: каждому запросу — идентификатор в X-Request-ID
        app.Use(requestid.New())

        // 2. Logger: идёт до статики, поэтому логирует и файлы из урока 5
        app.Use(logger.New(logger.Config{
                Format: "${time} ${requestid} ${status} ${latency} ${method} ${path} ${error}\n",
        }))

        // 3. Recover: паника станет ошибкой ДО возврата в logger,
        //    и он успеет записать её в ${error}
        app.Use(recover.New())

        // 4. Статика: существующие файлы уезжают отсюда, промах идёт дальше
        app.Use("/", static.New("./public"))

        // 5. API заметок — сквозной пример курса
        app.Get("/api/notes", func(c fiber.Ctx) error {
                return c.JSON(fiber.Map{"items": []string{"первая", "вторая"}})
        })

        log.Fatal(app.Listen(":3000"))
}

Четыре слоя и маршрут в порядке регистрации — этот каркас стоит запомнить как есть. requestid первым: его значение нужно логгеру для ${requestid}. Логгер вторым и обязательно до статики — обещание из урока 5 «Logger честно логирует и раздаваемые файлы» выполняется именно так: запросы к /css/style.css пройдут через его слой. Recover третьим, между logger и статикой: на обратном пути паника успевает превратиться в ошибку до кадра логгера. Дальше — статика и API: их порядок мы репетировали в прошлом уроке, здесь ничего не изменилось. Проверка curl-ом: /api/notes даёт JSON и строку лога, /css/style.css — файл и тоже строку лога, а curl -i покажет X-Request-ID в каждом ответе.

Порядок встроенных middleware: почему logger раньше recover#

Порядок в main.go выше не вкусовщина, а следствие луковицы — разберём, что сломается при перестановке. Пусть recover стоит раньше logger: цепочка «recover → logger → хендлер». Паника в хендлере разматывает стек вверх и пролетает сквозь кадр логгера, не останавливаясь: код после его c.Next() не исполняется никогда — выполнение функции прервано. Лог теряет строку о запросе целиком, а ErrorHandler вызывается уже fiber-ом на верхнем уровне. Теперь как в нашем main.go: «logger → recover → хендлер». Паника сначала достигает recover-слоя (он внутри), превращается в ошибку, и logger-овский c.Next() возвращается штатно — с ошибкой в руках. Логгер пишет ${error} и сам передаёт ошибку в ErrorHandler. Итог: с логом, без потерь.

Это тот же порядок, что используют соседние фреймворки в своих дефолтных сборках: в gin.Default() роутер получает Logger, а затем Recovery — по той же причине. RequestID ставится ещё раньше логгера, потому что тот хочет печатать его значение. Общее правило читается легко: читайте список app.Use сверху вниз как маршрут запроса снаружи внутрь — то, что должно видеть всё, вешайте раньше; то, что должно чинить падения, — сразу после наблюдателей.

Соберите каркас middleware

Расставьте строки main.go в правильном порядке: от создания приложения до запуска. Помните, кто кого должен видеть.

Строки

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

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

Допишите настройку встроенных слоёв

Вставьте пропуски: имя пакета, тег таймера, фабрика recover и тег ошибки.

func main() { app := fiber.New() // RequestID: идентификатор запроса в заголовке X-Request-ID app.Use(.New()) // Таймер latency тикает, только если тег есть в формате app.Use(logger.New(logger.Config{ Format: "${status} ${} ${method} ${path} ${}\n", })) // Recover: перехват паник из внутренних слоёв app.Use(recover.()) app.Get("/api/health", func(c fiber.Ctx) error { return c.JSON(fiber.Map{"ok": true}) }) log.Fatal(app.Listen(":3000")) }

Словарь урока

Термин

type Handler = func(Ctx) error

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

Единая сигнатура хендлеров и middleware в Fiber v3 (app.go); Ctx — интерфейс. В v2 контекст был указателем *fiber.Ctx.

1 / 6

Запуск, проверка и что дальше#

main.go из урока самодостаточен: создайте рядом папку public с index.html и стилем, запустите go run . и прогоните набор проверок. Первая — curl /api/notes: JSON в ответе и строка лога с requestid, статусом и латентностью в терминале. Вторая — curl /css/style.css: файл со статической раздачей и точно такая же строка лога — слои честно оборачивают и статику. Третья — curl -i на любой путь: в заголовках ответа появится X-Request-ID. Четвёртая — временно добавьте в хендлер panic("check") и убедитесь, что клиент получил 500 с телом «check», процесс жив, а в логе ошибка видна в ${error}. Финальный эксперимент: поменяйте местами logger и recover и найдите пропавшую строку лога на паникующем запросе. В браузерной песочнице сайта сетевые приложения не запускаются — runner-ов в уроках Fiber нет, проверяйте в терминале с локальным Go.

Следующий шаг по плану — свои middleware: напишем аутентификацию-заглушку, таймер и передачу данных между слоями руками, чтобы понять, из чего состоят готовые пакеты. Механика c.Next() и прерывания, которую вы отработали сегодня, — ровно тот инструмент. Про логирование в реальных сервисах — slog, корреляция по RequestID — будет отдельный урок ближе к концу курса; а освежить параметры запроса можно в уроке 4, полный список тем — на странице курса Fiber.

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

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

Вывод

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

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

  • Middleware в Fiber — обычный Handler func(Ctx) error (в v3 Ctx — интерфейс); подключается через app.Use, префикс, аргументы маршрута или монтирование приложения.
  • c.Next() передаёт эстафету следующему слою; код после него исполняется на обратном пути — модель луковицы: запрос внутрь, ответ наружу.
  • return err до c.Next() прерывает цепочку: ошибку обрабатывает ErrorHandler — *fiber.Error даёт свой код, обычная ошибка — 500 с телом err.Error().
  • logger.New() пишет access-log по формату из тегов ${status}, ${latency}, ${method}, ${path}, ${error}; таймер тикает только при наличии ${latency}, а ошибки цепочки логгер передаёт в ErrorHandler сам.
  • recover.New() превращает панику в ошибку и ответ 500; без него fasthttp роняет весь процесс — восстановления паник на уровне сервера нет.
  • requestid.New() ведёт X-Request-ID: валидный входящий ID переиспользуется, иначе генерируется SecureToken-ом; теги ${requestid} и ${request-id} доступны в логгере.
  • Рабочий порядок слоёв: requestid → logger → recover → статика → маршруты; логгер стоит раньше recover, чтобы не терять строки лога о паниках.

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

Возьмите SPA-сервер из урока 5 и добавьте к нему requestid, logger и recover. Составьте формат с ${requestid} и ${error}, проверьте curl -i заголовок X-Request-ID на API и на статике. Уроните один хендлер паникой и убедитесь, что клиент получил 500 с текстом паники, процесс жив, а лог записал ошибку. Затем поменяйте logger и recover местами и опишите одной фразой, что именно исчезло из лога. Наконец, добавьте в конфиг логгера Next, пропускающий /api/health, и объясните результат.

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

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

Чем middleware в Fiber отличается от middleware в Echo?

Сигнатурой. В Echo middleware — это func(next echo.HandlerFunc) echo.HandlerFunc: следующий слой приходит аргументом, и вы зовёте его как функцию — next(c). В Fiber middleware — обычный Handler func(fiber.Ctx) error, а эстафету передаёт метод контекста c.Next(). Обе модели рисуют одну и ту же луковицу, но при переезде между фреймворками перепутанный вызов — ошибка номер один.

Что будет при панике в хендлере, если не подключать recover?

Fasthttp не восстанавливает паники — в его server.go прямо сказано, что любая паника уронит весь сервер. Процесс завершится вместе со всеми обслуживаемыми запросами; перезапускать придётся внешнему супервизору. recover.New() перехватывает панику, превращает её в error, и ErrorHandler отвечает 500 (или кодом *fiber.Error, если паниковали ею).

Логгер подключен, но строки лога не появляются. Почему?

Чаще всего слой зарегистрирован после маршрутов: цепочка идёт по порядку стека, и хендлер, не вызывающий c.Next(), завершает обработку сам, не дойдя до логгера. Ставьте app.Use(logger.New()) до маршрутов и до статики. Вторая причина — в конфиге указан Skip, который отключает запись для сматченных запросов, при этом сам middleware продолжает работать.

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

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

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

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

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

Gin: самый популярный веб-фреймворк Go

Курс Gin для Go: маршрутизация, binding, валидация, middleware, JWT, WebSocket, тесты и деплой. 25 уроков с кодом и итоговым REST API.

  • Уверенно владеть gin.Engine: маршруты, группы, версии
  • Привязывать запросы к структурам (ShouldBindJSON и др.)
  • Писать middleware и использовать встроенные
  • Строить аутентификацию на JWT