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

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

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

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

Middleware и цепочки в Echo: e.Use, next(c), порядок и прерывание

Middleware в Echo — это обёртка над обработчиком: слой кода, который видит каждый запрос до того, как он дойдёт до вашей функции, и каждый ответ после. Один echo middleware заменяет десятки повторов: логи, таймеры, аутентификация, recovery. Разберём сигнатуру echo.MiddlewareFunc, подключение через e.Use и e.Pre, порядок луковицы, передачу данных через c.Set/c.Get, прерывание цепочки — и соберём связку логгер-таймер плюс аутентификация-заглушка на защищённой группе.

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

echo middleware: зачем обёртка над обработчиком#

В конце прошлого урока в каждом обработчике появилась одна и та же церемония: c.Bind, нормализация, c.Validate. А теперь представьте, что к этому добавилось логирование каждого запроса, замер времени ответа и проверка авторизации. Три if-а и два лога в каждом из двадцати обработчиков — и вот уже половина кода API это копипаста, которая расходится между обработчиками по мере правок.

Механизм echo middleware решает это структурно: слой — функция, которая принимает следующий обработчик цепочки и возвращает новый. Вы пишете логику один раз и подключаете её к маршруту, группе или всему серверу — а Echo сама проводит каждый запрос через все слои по очереди. Такая конструкция называется цепочка middleware: запрос проходит через слои снаружи внутрь, обработчик формирует ответ, и ответ идёт обратно через те же слои. Роутинг мы собирали в уроке про маршрутизацию, конвейер bind-validate — в уроке про привязку и валидацию; middleware — это третий слой архитектуры Echo, который оборачивает их все.

Сигнатура MiddlewareFunc и миф про c.Next#

В echo.go всё описано двумя строчками. type HandlerFunc func(c Context) error — так выглядят обработчики, к которым вы привыкли. А type MiddlewareFunc func(next HandlerFunc) HandlerFunc — middleware принимает следующий обработчик и возвращает новый. Смотрите, что здесь спрятано: в Echo следующий слой приходит вам аргументом next, и вы вызываете его как обычную функцию — next(c). Никакого метода c.Next() в Echo нет — ни в четвёртой версии, ни в пятой. c.Next() — это API Gin и Fiber, и его же пишут в старых постах про Echo по памяти о других фреймворках. Если такое встретите в рецепте про echo — рецепт либо устарел, либо скопирован из чужого фреймворка.

Второе, что стоит разглядеть в сигнатуре: MiddlewareFunc — это просто тип функции, и его удобно возвращать из фабрики. middleware.Logger() возвращает echo.MiddlewareFunc: сначала конфигурация, потом готовый слой. Ваши middleware будут строиться так же — либо голой функцией вида func(next echo.HandlerFunc) echo.HandlerFunc, либо фабрикой с настройками, которая возвращает такой же тип. Для совместимости с net/http-стеками в echo.go есть WrapMiddleware — он оборачивает классическую func(http.Handler) http.Handler в echo.MiddlewareFunc, но это уже мост в мир стандартной библиотеки, а не наш главный путь.

Логгер-таймер: код до next(c) и после

GO
func loggerTimer(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
                start := time.Now() // фаза запроса: до прохода по цепочке

                err := next(c)      // дальше по цепочке — к обработчику
                if err != nil {
                        c.Error(err) // глобальный обработчик сформирует ответ
                }

                stop := time.Now() // фаза ответа: всё уже отработало
                fmt.Printf("%s %s -> %d %s\n",
                        c.Request().Method, c.Path(),
                        c.Response().Status, stop.Sub(start))
                return err
        }
}

// подключение в main():
e.Use(loggerTimer)

Три обязательные детали. Первая: return func(c echo.Context) error — middleware возвращает замыкание, и именно оно исполняется на каждый запрос; внешняя функция вызывается один раз при подключении. Вторая: err := next(c) — мы сохраняем ошибку, которую вернули внутренние слои, чтобы вернуть её наверх дальше. c.Error(err) зовёт глобальный HTTPErrorHandler прямо здесь, как это делает штатный middleware.Logger в исходниках Echo. Третья: строки после next(c) исполняются, когда внутренние слои уже закончили — c.Response().Status к этому моменту содержит реальный статус ответа, а stop.Sub(start) — честное время обработки. Импорты: fmt, time, net/http для полного файла. Про песочницу: Echo не входит в браузерный движок сайта, поэтому весь код урока запускается локально — go run и curl, как и в прошлых уроках курса.

e.Use: порядок подключения — это порядок обёрток#

e.Use добавляет middleware в общий стек сервера. Сам метод тривиален: e.middleware = append(e.middleware, middleware...) — всё, что вы передали (а это variadic, можно перечислить несколько через запятую), дописывается в конец среза. Но интересное — в том, как Echo из этого среза собирает цепочку. Внутри ServeHTTP сработает applyMiddleware: цикл идёт с конца среза к началу, и каждый элемент оборачивает текущий обработчик. Результат: последний добавленный слой оказывается ближайшим к обработчику, а самый первый добавленный — самым внешним.

Поэтому порядок вызовов e.Use — это не декорация, а архитектурное решение. e.Use(loggerTimer), затем e.Use(authStub) означает: логгер видит запрос первым и провожает его последним, аутентификация — второй и её строка «после» сработает раньше логгеровой. Если поменять вызовы местами — изменится всё: кто первый поставил метку времени, кто последним видит статус ответа, кто обрабатывает ошибки соседей. Док-комментарий к e.Use в echo.go лаконичен: adds middleware to the chain which is run after router — после маршрутизатора. Это не про порядок соседей, а про то, что маршрутизация уже случилась, когда слои начинают работать. Про то, что бывает до маршрутизатора, — отдельная история ниже.

Какой порядок строк напечатает сервер

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

// два middleware и обработчик:
func a(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
                fmt.Println("A: до")
                err := next(c)
                fmt.Println("A: после")
                return err
        }
}
// b — то же самое, печатает "B: до" и "B: после"

// в main():
e.Use(a)
e.Use(b)
e.GET("/", func(c echo.Context) error {
        fmt.Println("H: обработчик")
        return c.NoContent(http.StatusOK)
})

Луковица: до next(c) — запрос, после — ответ#

Картину из predict-а называют луковицей: запрос проходит сквозь слои снаружи внутрь, обработчик сидит в центре, ответ выходит обратно сквозь те же слои. У каждого middleware поэтому две половины жизни. Код до next(c) работает в фазе запроса: здесь можно померить start, проверить аутентификацию, дополнить запрос данными, отклонить его совсем. Код после next(c) работает в фазе ответа: здесь есть итоговый статус, время обработки, и здесь можно дописать заголовки — при условии, что ответ ещё не отправлен клиенту.

Именно на этой модели стоят почти все штатные middleware Echo: таймер и статус у Logger-а живут по обе стороны next(c), у Recover-а — отложенный recover(), который ловит панику из внутренних слоёв, пока кадры unwind-ятся назад через цепочку. Отсюда же практическое правило: тот, кто должен видеть всё, добавляется первым. Логгер, добавленный последним, увидит только то, что не съели соседние слои. А тот, кто отклоняет запросы, ставится после логгера — тогда каждый отказ тоже попадёт в лог. Порядок в e.Use читайте как маршрут запроса снаружи внутрь.

Аутентификация-заглушка на Basic Auth

GO
// заглушка: в реальных проектах пары берут из базы,
// а вместо Basic Auth ставят JWT или сессии
func authStub(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
                user, pass, ok := c.Request().BasicAuth()
                if !ok || user != "alice" || pass != "secret123" {
                        // прерывание цепочки: обработчик запрос не увидит
                        return echo.NewHTTPError(
                                http.StatusUnauthorized,
                                "нужна basic-аутентификация",
                        )
                }

                c.Set("username", user) // данные для звеньев ниже
                return next(c)
        }
}

Слева от if — фаза запроса: разбор заголовка Authorization методом BasicAuth() обычного *http.Request (Echo не нужна своя обёртка, чтобы достать заголовок). Не прошли проверку — возвращаем echo.NewHTTPError(http.StatusUnauthorized, ...) и next(c) не вызываем: это прерывание цепочки, обработчик и все слои ниже не исполнятся. Прошли — кладём имя пользователя в контекст методом c.Set и пропускаем запрос дальше. Возвращаемое из next(c) значение обязано уйти наверх без изменений: снаружи сидят другие слои, например наш loggerTimer, и они ждут эту ошибку.

c.Set и c.Get: память на один запрос#

Аутентификация выяснила, кто стучится, — как об этом узнает обработчик? Для таких передач между звеньями у echo.Context есть пара c.Set(key string, val any) и c.Get(key string) any. Внутри — карта Map (в echo.go это type Map map[string]interface{}), которая создаётся лениво при первом c.Set и лежит прямо в контексте запроса. Больше того, Echo хранит контексты в пуле и переиспользует их между запросами — c.Reset чистит хранилище, поэтому данные, положенные в c.Set, живут ровно один запрос и никогда не перетекут в соседний.

Важно читать контракт типов: c.Get возвращает any, а не строку. Доставать значение надо type assertion-ом, лучше ok-формой: name, ok := c.Get("username").(string). Без неё assert на неожиданном типе роняет программу паникой. Ключи — строки, и на реальном проекте их держат в константах (ctxUsername = "username"), чтобы опечатка в двух местах не превратилась в баг, который молча отдаст пустую строку. Примеры из самих исходников Echo: CSRF-middleware кладёт токен через c.Set("csrf", token), а роутер при 405 через c.Get достаёт заголовок Allow. Это штатный способ нести данные между звеньями, а не хак.

Проверка: как достать то, что положили в c.Set

В authStub выполнено c.Set("username", "alice"). В обработчике нужно получить имя. Какая строка корректна?

Защищённая группа и обработчик с c.Get

GO
func listUsers(c echo.Context) error {
        // ok-форма: ключ могли не установить выше
        name, ok := c.Get("username").(string)
        if !ok {
                return echo.NewHTTPError(
                        http.StatusInternalServerError,
                        "authStub не сработал: ключ username отсутствует в контексте",
                )
        }
        return c.JSON(http.StatusOK, echo.Map{
                "message": "служебный список пользователей",
                "from":    name,
        })
}

listUsers читает из контекста то, что положила аутентификация, и отвечает JSON-ом. echo.Map — сокращение из echo.go для map[string]interface{}: удобно для сборных ответов, хотя для стабильных контрактов, как мы решили в уроке про ответы, лучше структуры. Проверка ok — это страховка на случай, когда middleware забыли подключить: маршруты группы без authStub дойдут сюда с пустым контекстом, и лучше получить явную ошибку в логе, чем поле from без значения. Внутренние кавычки в сообщении ошибки — обычные, Go-строка в двойных кавычках.

main(): глобальный слой и защищённая группа

GO
func main() {
        e := echo.New()

        // глобальный слой: каждый запрос, включая 404
        e.Use(loggerTimer)

        // группа с middleware: authStub получит только /admin/*
        admin := e.Group("/admin", authStub)
        admin.GET("/users", listUsers)

        // публичные маршруты: аутентификации нет
        e.GET("/todos", listTodos)
        e.POST("/todos", createTodo)

        e.Logger.Fatal(e.Start(":8080"))
}

Здесь встречаются три уровня подключения. e.Use(loggerTimer) — глобально: слой увидит вообще все запросы, включая те, что кончатся 404 — маршрутизатор в этом случае подставляет штатный NotFoundHandler, и цепочка всё равно проходит через глобальные слои. e.Group("/admin", authStub) — группа: middleware получает только маршруты, зарегистрированные через admin.GET/admin.POST и так далее; на /todos он не сработает. Третий уровень — route-level: e.GET(path, handler, authStub) — middleware для одного-единственного маршрута; он встаёт ближе всех к обработчику. Внутри группы порядок такой: глобальные слои снаружи, затем group-middleware, затем route-middleware, затем обработчик.

Куда встаёт middleware: уровни и момент#

СпособМомент в цепочкеВидит ли 404Типичное дело
e.Pre(mw)до маршрутизациида, и может изменить путьпереписать URL, добавить или убрать слэш
e.Use(mw)после маршрутизации, снаружи всехда — обработчиком станет NotFoundHandlerлоги, таймеры, recovery, метрики
e.Group("/admin", mw) или g.Use(mw)после глобальных, только для маршрутов группытолько внутри префикса: группа ставит catch-allаутентификация, права доступа
e.GET(path, h, mw)ближе всех к обработчикунетточечная логика одного маршрута

e.Pre: middleware до маршрутизации#

e.Use-слои начинают работу после того, как роутер нашёл маршрут: в ServeHTTP сначала вызывается Find по дереву маршрутов, и только потом обработчик оборачивается стеком e.middleware. Значит, повлиять на сам выбор маршрута из них уже нельзя. Для этого есть e.Pre — premiddleware. Тот же тип MiddlewareFunc, та же луковица, но вставлена она раньше: premiddleware обёртывает не найденный обработчик, а замыкание, внутри которого происходит маршрутизация. Док-комментарий: adds middleware to the chain which is run before router.

Практическое применение у Pre узкое и честное: всё, что должно случиться до выбора маршрута. Штатные примеры — middleware.Rewrite, AddTrailingSlash и RemoveTrailingSlash: они правят r.URL.Path до того, как роутер сверится с таблицей, поэтому /admin/users/ и /admin/users попадают на один маршрут вместо 404 у одного из них. Всё остальное — логи, таймеры, аутентификация — живёт в e.Use: там уже известно, какой маршрут выбран, есть c.Path() и есть доступ к данным маршрута. Забавное следствие устройства: при несуществующем пути premiddleware и e.Use-слои всё равно исполнятся — 404 в Echo это тоже обработчик, просто штатный, а не ваш.

Прерывание цепочки: return без next(c)#

Пока каждый слой зовёт next(c), запрос движется вглубь. Но middleware имеет полное право этого не делать — и это главный рычаг контроля в echo middleware. Прерывание выглядит как обычный return. Вариант первый: return err — вернуть ошибку, не дойдя до обработчика. Ошибка начинает раскручивать цепочку обратно: каждый внешний слой получает её как результат next(c), может что-то сделать (залогировать, обернуть) и возвращает дальше. В самом конце ServeHTTP видит err != nil и зовёт глобальный HTTPErrorHandler: для *echo.HTTPError клиент получит её код и message в JSON, для любой другой ошибки — 500 Internal Server Error.

Вариант второй: написать ответ самому и вернуть nil. c.JSON(http.StatusUnauthorized, ...) прямо в middleware, return nil — клиент получит ровно ваш ответ, глобальный обработчик не вызовется, потому что ошибка не родилась. Оба варианта легальны, но первый лучше сочетается с остальной системой: HTTPError несёт статус и сообщение, обработчик оформит ответ единообразно, а логгер снаружи увидит ошибку в возвращаемом значении. Общее у обоих вариантов одно: звенья ниже не исполняются. Обработчик даже не узнает, что к нему кто-то стучался.

Проверка: next(c) забыли, вернули nil

В middleware никто не вызвал next(c) и не записал ничего в ответ, функция вернула nil. Что увидит клиент?

Что покажет curl -i без аутентификации

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

// цепочка из main() выше:
// e.Use(loggerTimer); admin := e.Group("/admin", authStub)
// curl -i localhost:8080/admin/users
// без заголовка Authorization

Порядок, который нельзя путать: логгер раньше recovery#

Луковица делает некоторые порядки принципиально неправильными. Возьмём recovery — middleware с отложенным recover(), превращающий панику в ошибку 500. Паника раскручивает стек кадров по одному, и в каждом middleware код после next(c) просто не исполняется — кадр убит. Если recovery добавить раньше логгера (то есть recovery окажется снаружи), то паника из обработчика пролетит сквозь логгер, не дав ему напечатать строку, и остановится на recovery. Запрос уйдёт с 500, а в логе — ничего.

Именно поэтому в примере из доккомментария самого пакета echo порядок такой: e.Use(middleware.Logger()), затем e.Use(middleware.Recover()) — логгер снаружи, recovery внутри. Логгер печатает строку на каждый запрос, включая паникующий; recovery конвертирует панику в ошибку и возвращает её, логгер фиксирует статус 500. Тот же принцип для ваших слоёв: сначала то, что должно видеть всё, потом то, что может отклонить или уронить. И ещё один порядок в голове: c.Error(err) внутри middleware уже фиксирует ответ — после него писать в ответ поздно, Response становится committed, и любая следующая попытка WriteHeader даст в логе net/http предупреждение о superfluous WriteHeader.

Восстановите middleware аутентификации

Соберите authStub: типы middleware, доступ к запросу, статус ошибки и кладка данных в контекст.

func authStub(next echo.) echo. { return func(c echo.Context) error { user, pass, ok := c.().BasicAuth() if !ok || user != "alice" || pass != "secret123" { return echo.NewHTTPError(http., "нужна аутентификация") } c.("username", user) return next(c) } }

Соберите main() с цепочкой

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

Строки

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

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

Что дальше: встроенные middleware и свой слой#

Мы собрали руками то, что Echo даёт готовым: у пакета middleware есть Logger с конфигурируемым форматом, Recover с печатью стека, RequestID, Timeout, Gzip и ещё дюжина слоёв на все случаи — от CORS до rate limit; их разбор — тема следующего урока про встроенные middleware. Ваша связка логгер-таймер плюс authStub — не учебная игрушка: замена fmt.Printf на структурный логгер и заглушки на настоящую проверку паролей превращает её в боевой каркас.

И вернёмся к конвейеру из прошлого урока: bind → normalize → validate — три одинаковые строки в каждом обработчике. Это классический кандидат на обёртку: middleware, который прогоняет валидацию до того, как обработчик получит управление, — с Skipper-ом и передачей результата через c.Set. Попробуйте сделать это домашним упражнением — в задании ниже есть каркас. Навигация по курсу — на странице курса Echo; устройство ответов и статусов, вокруг которых всё это крутится, разобрано в уроке про запросы и ответы, а конвейер валидации, который теперь можно обернуть, — в уроке про привязку и валидацию.

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

Термин

echo.MiddlewareFunc

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

type MiddlewareFunc func(next HandlerFunc) HandlerFunc: middleware принимает следующий слой и возвращает новый обработчик. Возвращается из фабрик вроде middleware.Logger(); c.Next() в Echo не существует — следующий слой зовётся next(c).

1 / 6

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

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

Вывод

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

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

  • echo.MiddlewareFunc — это func(next echo.HandlerFunc) echo.HandlerFunc: следующий слой приходит аргументом и зовётся next(c); c.Next() — API Gin и Fiber, в Echo такого метода нет.
  • e.Use добавляет middleware в глобальный стек (после маршрутизации), e.Pre — в premiddleware (до): только Pre может повлиять на выбор маршрута, например переписать путь.
  • Порядок подключения = порядок обёрток: последний добавленный слой ближе всех к обработчику; Logger добавляют раньше Recover, чтобы паникующие запросы попадали в лог.
  • У middleware две половины: до next(c) — фаза запроса, после — фаза ответа с финальным статусом и временем; рядом с ответом следите за committed.
  • c.Set/c.Get — карта на один запрос для передачи данных между звеньями; c.Get возвращает any, доставайте ok-формой type assertion.
  • Прерывание цепочки: return HTTPError — ответ оформит глобальный обработчик; свой ответ плюс return nil — клиент получит ровно ваш вывод; в обоих случаях обработчик не исполняется.
  • Три уровня: e.Use глобально, e.Group/g.Use для префикса, route-level middleware для одного маршрута; у штатных слоёв есть Skipper, чтобы пропускать ненужные пути.

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

Разверните пример урока: добавьте middleware timingMetric, который печатает только запросы дольше 100 миллисекунд, и поставьте его между loggerTimer и группой — проверьте через curl, что он видит статус и время. Затем напишите validate-middleware: он берёт из контекста структуру, положенную туда до вызова next(c), прогоняет через c.Validate и на ошибку возвращает 400, не доходя до обработчика. Проверьте оба случая: аутентифицированный запрос к /admin/users и запрос без заголовка Authorization — в логе должны быть строки со статусами 200 и 401.

Личные заметкиПусто
Хранятся только в вашем браузере (Echo · Middleware и цепочки в Echo: e.Use, next(c), порядок и прерывание)

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

Чем e.Use отличается от e.Pre в Echo?

e.Use добавляет middleware, который исполняется после маршрутизации: уже известен выбранный маршрут, и слой обрабатывает все запросы, включая 404. e.Pre — premiddleware до роутера: он может изменить путь запроса (Rewrite, trailing slash) и тем самым повлиять на выбор маршрута, но не знает, какой маршрут совпадёт.

Почему в коде Echo нет c.Next()?

В Echo v4 и v5 middleware — это echo.MiddlewareFunc: функция, которая получает следующий обработчик аргументом next и возвращает новый HandlerFunc. Вызов идёт как next(c). Метод c.Next() есть в Gin и Fiber, и его ошибочно переносят в рецепты про Echo — рабочим такой код не будет.

Как остановить цепочку middleware и отдать ошибку?

Верните из middleware ошибку, не вызывая next(c): echo.NewHTTPError(http.StatusUnauthorized, "...") даст клиенту 401 с JSON-телом через глобальный обработчик ошибок, любая другая ошибка станет 500. Если нужен полностью свой ответ — напишите его через c.JSON и верните nil.

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

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

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

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

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

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

GORM: ORM-библиотека для Go

Курс GORM: модели, CRUD, ассоциации, транзакции, хуки, пагинация, оптимизация запросов. 25 уроков с примерами и проектом слоя данных.

  • Подключать БД и настраивать пул соединений
  • Описывать модели и связи: 1:1, 1:N, M:N
  • Писать CRUD и сложные выборки chainable-стилем
  • Контролировать N+1 через Preload