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

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

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

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

Свои middleware

Тема «кастомный middleware fiber» — это тот же Handler из шестого урока, только написанный под вашу задачу: аутентификация, таймер, передача данных между слоями. Разберём две формы собственного слоя (обычная функция и фабрика-замыкание, как у штатных пакетов), построим аутентификацию-заглушку с проверкой заголовка X-API-Key и JSON-ошибкой 401, замерим длительность запроса в error-стиле и передадим пользователя в хендлер через контекст запроса c.Locals. Все факты сверены с исходниками тега v3.5.0.

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

кастомный middleware fiber: три задачи, которые остаются вам#

В шестом уроке мы разобрали механику до последней строчки: Handler = func(Ctx) error, c.Next() и луковица, прерывание цепочки ошибкой, канонический порядок requestid → logger → recover. Встроенных пакетов хватает для наблюдения и защиты, но третий класс задач они не закрывают — бизнес-логика доступа к вашим данным. Логгер не знает, что ключ «s3cret» открывает заметки, а таймер не понимает, кому отдавать вычисленный пользователь. Это остаётся вам, и хорошая новость в том, что нового синтаксиса нет: свой middleware — это обычная функция или фабрика, возвращающая fiber.Handler.

Разберём три классические задачи, которые встречаются почти в каждом сервисе. Первая — аутентификация-заглушка: слой проверяет заголовок X-API-Key и не пускает чужих дальше, отвечая 401 с JSON-ошибкой. Вторая — таймер: замерить, сколько запрос провёл внутри цепочки, не потеряв ошибку. Третья — контекст запроса: передать вычисленное аутентификацией значение в хендлер через c.Locals. Сквозной пример курса — API заметок — продолжается: к концу урока всё API уедет под группу /api со стражем на входе. Все факты сверены с исходниками тега v3.5.0: app.go, router.go, req.go и каталог middleware.

Две формы своего middleware

GO
// Форма 1: обычная функция. Тип совпадает с fiber.Handler,
// поэтому её можно вешать на Use и маршруты напрямую.
func timerPlain(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
}

// Форма 2: фабрика. Конфигурация приходит аргументом,
// наружу возвращается готовый fiber.Handler.
func pathPrinter(prefix string) fiber.Handler {
        // замыкание видит prefix при каждом вызове слоя
        return func(c fiber.Ctx) error {
                fmt.Printf("%s: запрос %s %s\n", prefix, c.Method(), c.Path())
                return c.Next()
        }
}

// Подключение обеих форм выглядит одинаково
app.Use(timerPlain)
app.Use(pathPrinter("api"))

Фабрика — не особый синтаксис Fiber, а обычный Go: функция, возвращающая функцию того же типа Handler. Fiber не различает формы — на вход Use и маршрутов принимается любой хендлер, разница только в удобстве. Форма 1 хороша для слоёв без настройки, форма 2 — когда конфиг нужен: он захватывается замыканием и живёт столько, сколько живёт слой. Обратите внимание на возврат ошибки в обеих формах: err := c.Next(), затем return err — этот рисунок назовём error-стилем и разберём отдельно ниже.

Фабрика-замыкание: конфиг на входе, слой на выходе#

Зачем вообще вторая форма? Слою часто нужен конфиг: ожидаемый ключ, имя заголовка, порог срабатывания, адрес получателя лога. Обычная функция может взять это только из глобальных переменных — и вот у вас уже два сервиса дерутся за одну переменную. Фабрика решает проблему чисто: requireAPIKey("s3cret") и requireAPIKey("other") вернут два независимых слоя, каждый со своим ключом внутри замыкания. Конфиг живёт ровно столько, сколько нужен, не торчит в пакете и не тянется через параметры каждого вызова.

Второй аргумент за фабрику — единообразие со штатными пакетами. Вы уже видели этот рисунок в шестом уроке: logger.New(logger.Config{...}) принимает конфиг и возвращает fiber.Handler, то же делает recover.New(), requestid.New() и любое стороннее middleware Fiber. Промышленная версия чуть развитее: конфиг оформляется структурой Config с полем ConfigDefault и вариадиком New(config ...Config), поэтому незаполненные поля добираются из дефолта. Мы приходим к тому же интерфейсу, только с простым аргументом-строкой. Последнее правило гигиены: фабрику зовут один раз при регистрации, а не в хендлере на каждый запрос. Слой, созданный в хендлере, работает, но бессмысленно тратит память на замыкания и прячет конфигурацию там, где её не найти при чтении кода приложения.

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

Чем фабрика requireAPIKey(key) fiber.Handler лучше функции-слоя, читающей ключ из глобальной переменной пакета?

Аутентификация-заглушка: X-API-Key и JSON-ошибка 401

GO
// requireAPIKey возвращает слой-страж: без верного ключа путь закрыт
func requireAPIKey(key string) fiber.Handler {
        return func(c fiber.Ctx) error {
                // c.Get читает заголовок запроса без учёта регистра
                if c.Get("X-API-Key") != key {
                        // статус 401 и JSON-ошибка пишутся здесь, без ErrorHandler
                        return c.Status(fiber.StatusUnauthorized).JSON(
                                fiber.Map{"error": "неверный API-ключ"},
                        )
                }
                // ключ подошёл — эстафета дальше по цепочке
                return c.Next()
        }
}

// Подключение аргументом маршрута: защита только этого пути
app.Get("/api/notes", requireAPIKey("s3cret"), listNotes)

Три детали. Первая: c.Get возвращает значение заголовка запроса; в req.go подчёркнуто, что имена полей сравниваются без учёта регистра — X-API-Key, x-api-key и X-Api-Key для Fiber одно и то же. Вторая: fiber.StatusUnauthorized — константа для 401; c.Status возвращает сам контекст, поэтому к нему прицепляется c.JSON, и ответ уходит с Content-Type application/json. Третья: ответ записан самим слоем, return завершает функцию без c.Next() — хендлер listNotes не исполнится вовсе. Это прерывание цепочки из шестого урока, только с самодельным ответом вместо fiber.NewError.

fiber auth middleware: почему 401 вышел JSON, а не text/plain#

В шестом уроке прерывание делалось через fiber.NewError(fiber.StatusUnauthorized, ...): ошибка улетала в ErrorHandler приложения, и DefaultErrorHandler отвечал телом text/plain; charset=utf-8. Здесь мы выбрали другой путь — слой сам записал ответ через c.Status(...).JSON(...) и вернул результат как ошибку (а c.JSON возвращает error, которым обычно является nil). Для клиента разница материальная: JSON-тело удобно парсить фронтенду, text/plain — нет. Способов получить JSON-ошибку ровно два: писать ответ в самом middleware, как сейчас, или заменить ErrorHandler приложения — fiber.New(fiber.Config{ErrorHandler: ...}) — тогда и fiber.NewError начнёт отвечать JSON-ом по всему приложению. Первый способ дешевле для одного слоя, второй даёт единый формат ошибок сразу везде; к ErrorHandler мы вернёмся в уроке про обработку ошибок.

Раз строим аутентификацию, честно скажем: в комплекте Fiber есть и готовый fiber auth middleware — пакет github.com/gofiber/fiber/v3/middleware/keyauth. Его конфиг — зеркальное отражение того, что мы построили руками. Extractor достаёт ключ из запроса (дефолт — extractors.FromAuthHeader("Bearer"), для нашего случая подошёл бы extractors.FromHeader("X-API-Key")); Validator сверяет извлечённый ключ; SuccessHandler пускает дальше — дефолтно это return c.Next(); ErrorHandler отвечает при провале — дефолтно 401 и текст «missing or invalid API Key». Наша заглушка — не кустарщина, а упрощённая копия промышленного шаблона, и после этого урока чтение исходников keyauth не вызовет вопросов.

Клиент ошибся ключом: что в ответе?

Слой requireAPIKey("s3cret") подключен к /api/notes. Клиент отправил заголовок X-API-Key со значением "wrong". Что получит клиент?

Таймер- middleware: замер до и после c.Next()

GO
// timer возвращает слой, замеряющий длительность обработки запроса
func timer() fiber.Handler {
        return func(c fiber.Ctx) error {
                // фаза запроса: старт до всех внутренних слоёв
                start := time.Now()

                // эстафета: ошибка сохраняется, а не отбрасывается
                err := c.Next()

                // фаза ответа: замер выполняется и для ошибочных ответов
                fmt.Printf("%s %s -> %v\n", c.Method(), c.Path(), time.Since(start))

                // error-стиль: отдаём ошибку наверх без изменений
                return err
        }
}

// Подключение на всё приложение
app.Use(timer())

Луковица из шестого урока в чистом виде: код до c.Next() исполняется на пути запроса, код после — на пути ответа. Внутри штатного пакета responsetime ровно этот рисунок: start := time.Now(), err := c.Next(), затем c.Set(cfg.Header, time.Since(start).String()) и return err — он ставит заголовок X-Response-Time с длительностью каждого запроса (константа fiber.HeaderXResponseTime). Наш таймер — его урезанная версия с печатью в консоль вместо заголовка.

error-стиль: return err после замера#

Самая частая ошибка при написании таймера — записать return c.Next() одной строкой и поставить печать «после»: return покидает функцию немедленно, и до Printf выполнение не доходит никогда, замер молча исчезает. Правильный рисунок — три шага: err := c.Next(), замер, return err. Вторая граблина — return nil после замера при непустой ошибке: если внутренние слои вернули ошибку, а таймер её отбросил, ErrorHandler никогда её не увидит, клиент получит пустой 200 (ответ ведь никто не записал), а вышестоящий логгер не покажет ${error}. Ошибка в луковице всплывает наверх через каждый слой: ваш слой обязан либо обработать её и записать ответ, либо передать как есть. «Перехватил и забыл» — не приём, а бага.

Третье наблюдение тоньше: печать стоит после c.Next(), а не после проверки err != nil, поэтому длительность видна и для ошибочных ответов — в том числе для 401 из прошлого раздела. Слой-таймер честно меряет всё, что происходило внутри, независимо от исхода. Если хотите отдавать замер клиенту, добавьте c.Set("X-Response-Time", time.Since(start).String()) до return err — заголовок уедет с ответом; полный вариант с конфигурируемым именем заголовка — это responsetime.New() из комплекта, о котором сказано выше. И последнее: замер продолжит работать и на паникующих запросах, но только если recover-слой стоит ВНУТРИ таймера — об этом месте в порядке регистрации отдельно поговорим при сборке каркаса.

Где пропала строка замера?

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

func timer(c fiber.Ctx) error {
        start := time.Now()
        err := c.Next()
        if err != nil {
                // «заботливо» гасим ошибку
                return nil
        }
        fmt.Printf("%s %s -> %v\n", c.Method(), c.Path(), time.Since(start))
        return err
}

app.Use(timer)
app.Get("/boom", func(c fiber.Ctx) error {
        return fiber.NewError(fiber.StatusInternalServerError, "boom")
})

Контекст запроса: c.Locals как конверт между слоями#

Слои часто обязаны передавать друг другу данные: аутентификация вычислила пользователя — хендлеру нужен логин; таймер посчитал длительность — логгеру пригодилось бы число. Заводить под это глобальную карту — гонки и утечки между запросами. В Fiber для этого служит c.Locals. Сигнатура из v3: func (c fiber.Ctx) Locals(key any, value ...any) any — метод-двуликость: один аргумент означает чтение, два — запись. Докомментарий к интерфейсу формулирует назначение: makes it possible to pass any values under keys scoped to the request — значения живут ровно один запрос; документация добавляет, что они удаляются после завершения запроса, а под капотом стоят UserValue и SetUserValue fasthttp.

Чтение возвращает any — без магии: доставайте type assertion в ok-форме: user, ok := c.Locals("user").(string). Если ключа нет, вернётся nil, ok будет false, а user — пустая строка; прямое приведение c.Locals("user").(string) без ok на отсутствующем ключе паникует. Для типизированных чтений есть дженерик-версия fiber.Locals[string](c, "user") — она вернёт zero value типа, если ключа нет или тип не совпал. Ключом может быть любое значение; документация советует заводить непроэкспортируемый тип для ключа, чтобы разные пакеты не столкнулись строками. Ещё две детали из исходников: значение, реализующее io.Closer, Fiber закроет при удалении; а StoreInContext(c, key, value) пишет и в locals, и в context.Context — правда, второе только при включённом конфиге PassLocalsToContext (по умолчанию false).

Страж запоминает пользователя, хендлер читает

GO
// Слой-страж: после успешной проверки пишет в контекст запроса
func requireAPIKey(key string) fiber.Handler {
        return func(c fiber.Ctx) error {
                if c.Get("X-API-Key") != key {
                        return c.Status(fiber.StatusUnauthorized).JSON(
                                fiber.Map{"error": "неверный API-ключ"},
                        )
                }
                // запись в locals: два аргумента
                c.Locals("user", "api-client")
                return c.Next()
        }
}

// Хендлер читает то, что оставил слой: один аргумент + type assertion
app.Get("/api/me", func(c fiber.Ctx) error {
        user, ok := c.Locals("user").(string)
        if !ok {
                return c.Status(fiber.StatusUnauthorized).JSON(
                        fiber.Map{"error": "нет пользователя в контексте"},
                )
        }
        return c.JSON(fiber.Map{"user": user})
})

Ключом здесь служит строка «user» — так короче для урока; в пакете с несколькими слоями возьмите непроэкспортируемый тип из совета документации. ok-форма обязательна по двум причинам: ключ мог не быть записан (этот маршрут вызван мимо стража), а чтение возвращает any. Если ok false, хендлер честно отвечает 401 — без паники и без мусорных данных. На живом сервере под «user» лежала бы запись из БД, а не строка-константа; механика от этого не меняется.

Чтение несуществующего ключа

Ни один слой не записывал ничего под ключом «ghost». Что произойдёт при вызове c.Locals("ghost")?

Групповая привязка: app.Group("/api", mw...)#

Вешать стража на каждый маршрут по отдельности — работа для робота, и она же порождает главный источник дыр: один новый маршрут забыли прикрыть. app.Group собирает маршруты с общим префиксом и вешает на них middleware одним движением. Сигнатура из app.go: func (app *App) Group(prefix string, handlers ...any) Router, а докомментарий звучит так: Group is used for Routes with common prefix to define a new sub-router with optional middleware. Возвращается Router — под-роутер с привычным набором методов: api.Get, api.Post, вложенные api.Group("/v1"). Слои из Group получают все маршруты, зарегистрированные на группе и на вложенных группах, — и только они: статика из урока 5 и корневые пути вроде health-check остаются без аутентификации, что обычно и требуется.

Две грабли из исходников. Первая: handlers — вариадик any, и незнакомый тип паникует в collectHandlers с контекстом «group»: сообщение вида group: invalid handler #0 (...тип...) — родственник «use: invalid handler» из шестого урока. Вторая — порядок регистрации: Group регистрирует свои слои в момент вызова, поэтому маршруты, объявленные РАНЬШЕ вызова app.Group, слоёв не получают. Это та же стековая механика, что у Use: слой, записанный в стек позже живого маршрута, не оборачивает его. Привычка, которая спасает: сначала все Use и Group, потом маршруты групп, Listen — последней строкой.

Каркас сервиса: встроенные слои плюс своя группа

GO
package main

import (
        "fmt"
        "log"
        "time"

        "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"
)

// Таймер: замер вокруг всей внутренней цепочки
func timer() fiber.Handler {
        return func(c fiber.Ctx) error {
                start := time.Now()
                err := c.Next()
                fmt.Printf("%s %s -> %v\n", c.Method(), c.Path(), time.Since(start))
                return err
        }
}

// Страж: проверка X-API-Key, JSON-ошибка и запись в locals
func requireAPIKey(key string) fiber.Handler {
        return func(c fiber.Ctx) error {
                if c.Get("X-API-Key") != key {
                        return c.Status(fiber.StatusUnauthorized).JSON(
                                fiber.Map{"error": "неверный API-ключ"},
                        )
                }
                c.Locals("user", "api-client")
                return c.Next()
        }
}

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

        // 1-2: встроенные слои из шестого урока
        app.Use(requestid.New())
        app.Use(logger.New(logger.Config{
                Format: "${time} ${requestid} ${status} ${latency} ${method} ${path} ${error}\n",
        }))

        // 3: таймер — СНАРУЖИ recover, чтобы замерять и паникующие запросы
        app.Use(timer())
        app.Use(recover.New())

        // 4: защищённая группа — страж видят все маршруты /api
        api := app.Group("/api", requireAPIKey("s3cret"))
        api.Get("/notes", func(c fiber.Ctx) error {
                return c.JSON(fiber.Map{"items": []string{"первая", "вторая"}})
        })
        api.Get("/me", func(c fiber.Ctx) error {
                user, ok := c.Locals("user").(string)
                if !ok {
                        return c.Status(fiber.StatusUnauthorized).JSON(
                                fiber.Map{"error": "нет пользователя в контексте"},
                        )
                }
                return c.JSON(fiber.Map{"user": user})
        })

        // 5: health-check без ключа — маршрут вне группы
        app.Get("/healthz", func(c fiber.Ctx) error {
                return c.JSON(fiber.Map{"ok": true})
        })

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

Порядок нарочно отличается от канонического каркаса шестого урока в одном месте: таймер встал МЕЖДУ logger и recover. Причина — луковица. Паника в хендлере разматывает стек до ближайшего defer с recover(); если recover стоит внутри таймера, паника пролетает сквозь кадр таймера, и строка замера не печатается. Когда таймер снаружи, recover первым превращает панику в ошибку, c.Next() таймера возвращается штатно с этой ошибкой, замер печатается, а логгер выше пишет ${error}. Страж на группе /api защищает оба маршрута и пишет пользователя в locals; /healthz открыт, потому что зарегистрирован вне группы. Лог на запрос с неверным ключом покажет статус 401 при пустом ${error}: страж вернул nil, записав ответ сам.

Маршрут раньше группы: кто ответит?

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

// Маршрут объявлен ДО группы со стражем
app.Get("/api/notes", func(c fiber.Ctx) error {
        return c.JSON(fiber.Map{"items": []string{"первая"}})
})

// Группа и страж объявлены ПОСЛЕ
api := app.Group("/api", requireAPIKey("s3cret"))
api.Get("/users", listUsers)

Куда вешать свой слой: четыре варианта#

СпособПримерКто получит слой
Use на всё приложениеapp.Use(timer())Все запросы, зарегистрированные после него
Группа с префиксомapp.Group("/api", requireAPIKey(k))Все маршруты группы и вложенных групп
Аргументы маршрутаapp.Get("/notes", mw, handler)Только этот маршрут: слой в route.Handlers
Вложенная группаv1 := api.Group("/v1", mw)Маршруты /api/v1: слои родителя плюс свои

Соберите защищённую группу

Расставьте строки в порядке регистрации: встроенные слои, затем группа со стражем, затем маршрут и запуск. Помните, кто кого должен оборачивать.

Строки

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

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

Допишите фабрику стража

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

func requireAPIKey(key string) fiber.Handler { return func(c fiber.Ctx) error { // заголовок запроса; регистр имени не важен if c.Get("") != key { // свой JSON-ответ со статусом 401 return c.Status(fiber.).JSON( fiber.Map{"error": "неверный API-ключ"}, ) } // запоминаем пользователя в контексте запроса c.("user", "api-client") // эстафета дальше по цепочке return c.() } }

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

Термин

фабрика-замыкание

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

Функция, принимающая конфиг и возвращающая fiber.Handler: замыкание захватывает настройки, слой создаётся один раз при регистрации. Так устроены logger.New, recover.New и keyauth.New.

1 / 6

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

Соберите main.go из каркаса выше в один файл — timer и requireAPIKey лежат рядом с main, импорты уже перечислены — и прогоните сценарий curl-ом. Первый запрос: curl -H "X-API-Key: s3cret" /api/notes — в ответе JSON со списком, в консоли строка таймера с длительностью, в логе статус 200. Второй: тот же путь без заголовка — клиент получает 401 и {"error":"неверный API-ключ"}, лог пишет 401 при пустом ${error}: страж записал ответ сам и вернул nil. Третий: curl -i /healthz — 200 без всякого ключа, маршрут вне группы; заодно в заголовках видно X-Request-ID из шестого урока. Четвёртый, для любопытных: добавьте в таймер c.Set("X-Response-Time", time.Since(start).String()) и найдите этот заголовок в curl -i — у вас получился мини-клон responsetime.

Дальше по плану — восьмой урок: CORS и безопасность заголовков, как разрешить браузеру чужого origin дёргать ваше API и какие заголовки за это отвечают; таймер и страж из этого урока будут помогать его разбору. Освежить механику c.Next(), луковицы и ErrorHandler — в шестом уроке, параметры запроса — в четвёртом, полный план — на странице курса Fiber. В браузерной песочнице сайта сетевые приложения не запускаются — runner-ов в уроках Fiber нет, проверяйте примеры в терминале с локальным Go.

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

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

Вывод

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

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

  • Свой middleware — обычная функция func(Ctx) error или фабрика, возвращающая fiber.Handler: конфиг захватывается замыканием, слой создаётся один раз при регистрации, как у logger.New и recover.New.
  • Аутентификация-заглушка: c.Get("X-API-Key") читает заголовок без учёта регистра; при несовпадении слой сам пишет c.Status(fiber.StatusUnauthorized).JSON(...) и завершает обработку без c.Next().
  • fiber.NewError отвечает text/plain через DefaultErrorHandler; для JSON-ошибок либо пишите ответ в middleware, либо ставьте свой ErrorHandler в fiber.New(fiber.Config{ErrorHandler: ...}).
  • Таймер в error-стиле: start := time.Now() до c.Next(), err := c.Next(), замер, return err — замер видит и ошибочные ответы; return c.Next() убивает печать, return nil теряет ошибку.
  • c.Locals(key, value) пишет, c.Locals(key) читает any с type assertion в ok-форме; значения живут один запрос; есть дженерик fiber.Locals[V] и StoreInContext для context.Context.
  • app.Group("/api", mw) возвращает Router и вешает слои на все маршруты группы и вложенных групп; незнакомый тип паникует «group: invalid handler».
  • Порядок регистрации решает: маршрут, объявленный раньше Group или Use, слоёв не получает; таймер ставят снаружи recover, чтобы замерять и паникующие запросы.

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

Возьмите каркас из урока и доработайте его: вынесите ключ стража в переменную окружения и прокомментируйте, что изменилось в вызове фабрики; добавьте в таймер заголовок X-Response-Time и проверьте его через curl -i; заведите вторую группу /admin со вторым стражем и другим ключом, убедитесь, что замыкания не пересекаются; добавьте маршрут /api/secret ДО объявления группы и объясните результат запроса без ключа; наконец подключите готовый keyauth с extractors.FromHeader("X-API-Key") и собственным Validator и перечислите, какие части нашей заглушки он повторяет.

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

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

Когда нужна фабрика middleware, а когда достаточно обычной функции?

Обычной функции хватает, если слою не нужен конфиг — как у таймера без настроек. Как только появляется параметр (ключ, порог, адрес лога), берите фабрику: замыкание изолирует конфиг, можно создать несколько независимых слоёв с разными настройками, и регистрация читается одной строкой requireAPIKey("s3cret"). Именно так устроены штатные logger.New, recover.New и keyauth.New.

Как сделать, чтобы fiber.NewError отвечал JSON, а не text/plain?

Заменить ErrorHandler приложения: fiber.New(fiber.Config{ErrorHandler: func(c fiber.Ctx, err error) error { ... }}) — внутри достаньте код через errors.As в *fiber.Error и верните c.Status(code).JSON(...). Альтернатива для одного слоя — записать ответ прямо в middleware через c.Status(...).JSON(...) и вернуть результат, не трогая глобальный обработчик.

Почему таймер ставят раньше recover, а не после?

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

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

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

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

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

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

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

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

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

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