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

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

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

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

Встроенные middleware в Echo: Logger, Recover, RequestID, Timeout, Gzip

echo logger — это не одна функция, а целый каталог: пакет middleware в Echo даёт готовые слои на каждый сквозной процесс — логи, восстановление после паники, идентификатор запроса, таймауты, сжатие, статику. Разберём Logger и его fasttemplate-формат, RecoverConfig, RequestID, историю Timeout и замену ContextTimeout, Gzip с порогом MinLength, Static и общий механизм Skipper для выключения слоя на отдельных путях.

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

echo logger: каталог штатных middleware#

В прошлом уроке мы собрали loggerTimer руками: метка start, err := next(c), c.Error(err) на ошибку, печать статуса и времени после. У Echo всё это уже есть в коробке: пакет github.com/labstack/echo/v4/middleware — каталог готовых слоёв, обёрнутых вокруг next(c) той же сигнатурой echo.MiddlewareFunc. Двадцать с лишним файлов: Logger и RequestLogger, Recover, RequestID, Timeout и ContextTimeout, Gzip, Static, плюс CORS, CSRF, BasicAuth, KeyAuth, RateLimiter, BodyLimit, Redirect, Proxy и другие. Единственное заметное отсутствие — JWT: в четвёртой версии его вынесли в отдельный модуль labstack/echo-jwt, и это скорее плюс — модуль живёт своим циклом обновлений.

Все встроенные слои устроены по одному шаблону, и его стоит выучить как таблицу умножения: структура конфига XConfig с полем Skipper, пакетная переменная DefaultXConfig с дефолтами, две фабрики — X() с настройками по умолчанию и XWithConfig(config) для кастомизации. Внутри каждой фабрики — те же две фазы, что вы писали сами: код до next(c) и код после. В этом уроке разберём echo logger и echo recover под микроскопом, затем RequestID, семейство таймаутов, Gzip и Static, а механизм Skipper — общий для всех конфигов — покажем как штатный способ выключать слой для отдельных путей.

Минимум, который стоит ставить всегда

GO
package main

import (
        "net/http"

        "github.com/labstack/echo/v4"
        "github.com/labstack/echo/v4/middleware"
)

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

        // id запроса: Echo поставит заголовок X-Request-Id в ответ
        e.Use(middleware.RequestID())
        // строка лога на каждый запрос
        e.Use(middleware.Logger())
        // паника в любом звене цепочки превратится в ошибку 500
        e.Use(middleware.Recover())

        e.GET("/ping", func(c echo.Context) error {
                return c.String(http.StatusOK, "pong")
        })

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

Три слоя из каталога — и сервер уже ведёт себя как взрослый. Порядок не случайный: RequestID раньше Logger, чтобы идентификатор успел попасть в строку лога через тег ${id}; Logger раньше Recover — правило прошлого урока, иначе паникующий запрос уйдёт из лога. Пример — расширенный вариант доккомментария самого echo.go, где стоят Logger и Recover, а порт 1323 — фирменный из документации Echo. Пакет подключается отдельным импортом и сам себя не «оживляет»: каждый слой добавляется явным e.Use. Про песочницу: как и весь курс Echo, этот урок исполняется локально — нужен go mod init и go get github.com/labstack/echo/v4, дальше go run и curl: браузерный движок сайта сторонние модули не исполняет.

echo logger: анатомия формата Logger#

LoggerConfig — это Skipper, Format, CustomTimeFormat, CustomTagFunc и Output. Всю работу делает Format: строка-шаблон в синтаксисе fasttemplate, где теги записываются как ${имя} и компилируются вызовом fasttemplate.New(Format, "${", "}"). Дефолтный Format в DefaultLoggerConfig — однострочный JSON (здесь сокращён): {"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","method":"${method}","uri":"${uri}","status":${status},"latency_human":"${latency_human}"} и так далее.

Каталог тегов стоит один раз пробежать глазами. Время: time_unix, time_rfc3339_nano и time_custom, который берёт шаблон из CustomTimeFormat (дефолт — "2006-01-02 15:04:05.00000"). Запрос: id — это как раз X-Request-Id; remote_ip идёт через c.RealIP; дальше method, uri, path, route — шаблон маршрута вида /users/:id, protocol, referer, user_agent. Ответ: status (в терминале окрашивается: 2xx зелёным, 3xx бирюзовым, 4xx жёлтым, 5xx красным), error, latency в наносекундах и latency_human для людей, bytes_out из размера ответа. Отдельная группа — динамические теги: header:Authorization, query:user_id, form:action, cookie:session_id — значение берётся прямо из запроса.

Две детали, которые легко упустить. bytes_in — не измеренный размер тела, а заголовок Content-Length запроса: его может не быть вовсе, тогда в лог уйдёт 0. А скелет Logger-а — это ваш loggerTimer до строчки: err = next(c), при ошибке c.Error(err), замер stop.Sub(start) — только вместо fmt.Printf здесь буфер из sync.Pool и запись через Output, по умолчанию в вывод логгера Echo.

Свой формат: LoggerWithConfig

GO
// человекочитаемый формат вместо дефолтного JSON:
// время, статус, метод, шаблон маршрута, задержка
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
        Format:           "${time_custom} ${status} ${method} ${route} (${latency_human})
",
        CustomTimeFormat: "2006-01-02 15:04:05",
}))

route вместо uri удобнее для API: в логе будет шаблон /users/:id, а не конкретный /users/42 — одинаковые маршруты склеиваются глазами мгновенно. time_custom берёт верстку времени из CustomTimeFormat — это обычная Go-маска из пакета time с эталонной датой 2006-01-02. \n в конце обязателен: Echo не дописывает перевод строки за вас. И одна предосторожность из исходников: если Format начинается с фигурной скобки, Echo включает JSON-экранирование строк — в logger.go для этого отдельная функция writeJSONSafeString, чтобы самодельный JSON-формат не развалился от кавычки в uri или в тексте ошибки.

echo recover: конфиг, стек и LogLevel#

echo recover из коробки — это Recover() без аргументов, и для большинства проектов этого достаточно: отложенный recover() ловит панику в любом звене цепочки, пишет стек в лог и отдаёт ошибку глобальному обработчику — клиент получает 500 {"message":"Internal Server Error"}. Но RecoverConfig стоит знать подробнее. StackSize — размер буфера под runtime.Stack, по умолчанию 4 KB (в исходнике записано как 4 << 10): можно урезать для коротких стеков или поднять для глубоких. DisableStackAll — по умолчанию печатается стек всех горутин программы; true оставит только текущую. DisablePrintStack — вообще без стека. LogLevel — уровень, которым уходит сообщение вида [PANIC RECOVER] ошибка стек: по умолчанию Print, доступны DEBUG, INFO, WARN, ERROR и OFF (тишина). Если значение паники не реализует error, Echo аккуратно заворачивает его в fmt.Errorf("%v", r) — паника строкой не потеряет текст.

И два крючка для своих нужд. LogErrorFunc func(c echo.Context, err error, stack []byte) error заменяет штатную печать: шлите стек в трекер ошибок или структурный логгер; тонкость в том, что если ваша функция вернёт nil, глобальный обработчик ошибок вызван не будет — обработка целиком на вас. DisableErrorHandler, наоборот, выключает c.Error и возвращает ошибку из middleware наверх по цепочке, как будто Recover-а и не было.

Проверка: паника со значением ErrAbortHandler

В обработчике выполнено panic(http.ErrAbortHandler). Что сделает штатный middleware.Recover()?

RequestID: сквозной идентификатор запроса#

RequestID решает задачу, которая звучит скромно, а по цене исполнения дороже половины каталога: одна жалоба пользователя — и нужно найти в логах все её следы. RequestIDConfig включает Skipper, Generator, RequestIDHandler и TargetHeader. Механика в четыре строки: идентификатор читается из заголовка запроса; если клиент ничего не прислал — вызывается Generator; заголовок ставится в ответ до вызова next(c); если задан RequestIDHandler, он получает (c, id) — удобное место, чтобы положить id в контекст или метрики.

Генератор по умолчанию — randomString(32): строка из 32 символов, набранных из 52 латинских букв верхнего и нижнего регистра, источник — криптогенератор crypto/rand. Не hex и не UUID — именно буквы; для склейки логов этого достаточно. TargetHeader по умолчанию — echo.HeaderXRequestID, и значение этой константы в исходниках записано как X-Request-Id, таково каноническое написание Echo. Читать можно в любом регистре: Header.Get приводит имя к канонической форме, поэтому curl -H "X-Request-ID: ..." сработает как надо. Сквозная трассировка выходит сама собой: шлюз присылает свой X-Request-Id — Echo переиспользует его и возвращает в ответе, а тег ${id} у Logger-а читает заголовок из запроса и, на случай другого порядка слоёв, из заголовка ответа.

Клиент прислал свой X-Request-Id

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

e.Use(middleware.RequestID())

e.GET("/ping", func(c echo.Context) error {
        return c.String(http.StatusOK, "pong")
})

// запрос и ответ:
// curl -i -H "X-Request-Id: trace-42" localhost:1323/ping

Skipper: один механизм выключения для всех#

Каждый конфиг каталога начинается с одного и того же поля: Skipper Skipper — тип из middleware.go: type Skipper func(c echo.Context) bool. Вернули true — слой пропускается: первая же строка замыкания делает return next(c) без всякой работы. Дефолт — DefaultSkipper, функция, которая всегда возвращает false: «никого не пропускать». Рядом в том же файле описан BeforeFunc func(c echo.Context) — крючок, вызываемый прямо перед слоем; встречается реже, но это часть того же общего словаря пакета.

Практика скиперов — список мест, где слой не нужен. Health-чеки и метрики не должны замусоривать лог; мелкую статику незачем сжимать; аутентификация не нужна на публичных путях. Функция скипера — обычный код: сравните c.Request().URL.Path с "/healthz" или возьмите c.Path() — шаблон маршрута, который вы видели в теге ${route}. Так сделано в доккомментарии самого Logger-а: Skipper: func(c echo.Context) bool { return c.Request().URL.Path == "/health" }. Тот же паттерн стоит копировать в своих слоях из прошлого урока: поле Skipper в конфиге, проверка первой строкой. Слой, который нельзя выключить, быстро начинают обходить стороной.

Timeout: что случилось с middleware.Timeout#

Timeout — самый поучительный экспонат каталога, потому что он умер в чести. Появился в Echo v4.2.0 (начало 2021-го) как обёртка над http.TimeoutHandler из стандартной библиотеки. TimeoutConfig нёс поля Skipper, Timeout (0 — без таймаута), ErrorMessage и OnTimeoutRouteErrorHandler. Поведение такое: клиент, не дождавшийся ответа за Timeout, получает 503 Service Unavailable с ErrorMessage в теле. Но в доккомментарии заглавными буквами напечатан нюанс: NB: timeout does not stop handler execution — обработчик продолжает дорабатывать. 503 — это не отмена работы, а лишь сообщение о её задержке.

Свежие версии v4 пошли дальше: Timeout и TimeoutWithConfig помечены Deprecated, а файл timeout.go открывается многострочным WARNING: middleware causes more problems than it solves. Причина — подмена response writer между горутинами: слой обязан стоять первым в цепочке, иначе возможна data race. Официальная замена — ContextTimeout: context.WithTimeout от контекста запроса плюс c.SetRequest с новым контекстом. Гонок нет, позиция в цепочке любая; 503 возникает, когда внутренняя операция уважает контекст и вернёт context.DeadlineExceeded — ErrorHandler по умолчанию превращает её в echo.ErrServiceUnavailable, пряча внутреннюю ошибку от клиента, но оставляя её вашему обработчику ошибок.

ContextTimeout: дедлайн через контекст

GO
// дедлайн на весь запрос: 2 секунды
e.Use(middleware.ContextTimeout(2 * time.Second))

e.GET("/report", func(c echo.Context) error {
        // контекст с дедлайном достаётся из запроса
        ctx := c.Request().Context()

        // тяжёлая работа обязана принимать ctx:
        // так умеют sql, http-клиенты и драйверы
        rows, err := db.QueryContext(ctx, "SELECT ...")
        if err != nil {
                // дедлайн истёк -> 503 Service Unavailable
                if errors.Is(err, context.DeadlineExceeded) {
                        return echo.NewHTTPError(
                                http.StatusServiceUnavailable, "timeout")
                }
                return err
        }
        defer rows.Close()
        return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
})

ContextTimeout сам ничего не пишет клиенту — он подменяет контекст запроса и ждёт, что внутренние операции отреагируют на дедлайн. Дедлайн «долетает» до драйвера БД и HTTP-клиентов, потому что они принимают ctx первым аргументом; не принимает — дедлайн повиснет в воздухе, и 503 никто не вернёт. В конфиге ContextTimeoutConfig нулевой Timeout — ошибка конфигурации: фабрика вернёт ошибку, и ContextTimeoutWithConfig запаникует на старте. У старого Timeout ноль означал «выключен» — ещё одно различие, о котором молчат старые статьи.

Проверка: поведение старого Timeout

В цепочке стоит e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{Timeout: 100 * time.Millisecond, ErrorMessage: "later"})). Обработчик выполняется 3 секунды. Что увидит клиент и что станет с обработчиком?

Gzip: сжатие с буфером и порогом#

Gzip из файла compress.go показывает, сколько механики может прятаться за одним e.Use. Срабатывание честное: заголовок Accept-Encoding запроса должен содержать gzip (в исходнике константа gzipScheme = "gzip"); а заранее, до всякой проверки, в ответ добавляется Vary: Accept-Encoding — кэшам объявлено, что ответ зависит от этого заголовка. Дальше интереснее: Echo подменяет writer ответа обёрткой gzipResponseWriter, сами gzip.Writer переиспользуются из sync.Pool, WriteHeader откладывается до первой записи тела, Content-Length удаляется — заранее длину сжатого ответа знать невозможно, — а заголовок Content-Encoding: gzip ставится в момент, когда буфер перерастает порог MinLength.

MinLength — единственное поле, которое стоит настраивать руками. Ответ короче порога буферизуется и уходит как есть, без Content-Encoding и с исходной длиной (возврат к чистому состоянию отмечен в исходнике issues #424 и #407); вызов Flush принудительно включает сжатие, поэтому стриминг не ломается. Level — уровень сжатия: дефолт -1, то есть gzip.DefaultCompression, шкала от gzip.BestSpeed до gzip.BestCompression. И гост-ловушка: gzip.NoCompression — это 0, но ноль в int-конфиге Echo неотличим от «поле не задано», поэтому GzipConfig{Level: 0} молча станет дефолтными -1. Отключать сжатие для отдельных путей положено Skipper-ом, а не уровнем.

Ответ короче порога MinLength

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

e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
        Level:     6,
        MinLength: 128,
}))

e.GET("/status", func(c echo.Context) error {
        return c.String(http.StatusOK, "ok")
})

// запрос:
// curl -i -H "Accept-Encoding: gzip" localhost:1323/status

Проверка: GzipConfig{Level: 0}

Вы поставили middleware.GzipConfig{Level: 0} в надежде отключить сжатие — в compress/gzip значение 0 значит NoCompression. Что произойдёт?

Static: файлы, SPA-фоллбэк и Browse#

StaticConfig требует только Root — каталог, из которого раздаётся статика; пустое значение заменяется на "." с комментарием про безопасность прямо в исходнике. Index задаёт индексный файл каталога (по умолчанию index.html), Browse разрешает листинг — HTML-страница собирается из шаблона, встроенного прямо в static.go, — а IgnoreBase пригождается при подключении к группе с префиксом: без него путь на диске задваивается. Файловая система по умолчанию — http.Dir(config.Root), отдача идёт через http.ServeContent: поддержка Range и Last-Modified достаётся бесплатно.

Самое важное в Static — поведение промаха. Файла нет? Слой вызывает next(c): API-маршруты продолжают жить, будто Static-а нет вовсе. Режим HTML5 — фоллбэк для одностраничных приложений: если next(c) вернул 404, Static отдаёт index.html, и маршрутизацию подхватывает клиентский фреймворк. Отсюда дисциплина: HTML5: true включайте на группе, которая обслуживает фронтенд, а не глобально — иначе собственный API-404 начнёт приходить клиенту в виде index.html. И историческая справка: метод e.Static(prefix, root), существовавший с v4.0.0, удалён в v4.7.0 — рецепты с ним не компилируются; современный путь — e.Use(middleware.Static("public")). Наконец, EnablePathUnescaping по умолчанию false: закодированные слэши %2f не декодируются — подменой пути нельзя обойти маршруты с проверкой доступа.

Сжатие и статика вместе

GO
// Gzip раньше Static: статика тоже уйдёт сжатой
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
        Level:     6,      // середина шкалы gzip
        MinLength: 256,    // короче — отдать без сжатия
}))

// статика из каталога public;
// файл не найден — запрос идёт дальше по цепочке
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
        Root:  "public",
        Index: "index.html",
}))

Порядок здесь смысловой: Gzip подключён раньше Static, то есть снаружи — writer уже подменён сжимающей обёрткой, и найденные файлы уходят клиенту сжатыми. Поменяете местами — Static станет отдавать файлы напрямую в оригинальный writer, минуя сжатие. MinLength 256 отсекает мелочь, на которой сжатие проигрывает накладным расходам. Проверяется двумя curl: curl -i localhost:1323/ покажет заголовки отдачи файла, а curl --compressed localhost:1323/ — распаковку на клиенте. Если отдаёте SPA, добавьте HTML5: true на группу фронта — не глобально, помните про API-404.

Каталог пакета middleware Echo v4#

MiddlewareЧто делаетКлючевые поля конфига
Loggerстрока лога на каждый запрос: формат из тегов fasttemplate, окраска статуса, вывод в io.WriterFormat, CustomTimeFormat, CustomTagFunc, Output
RequestLoggerтот же лог новым API: поля включаются флагами Log*, значения собирает ваш LogValuesFuncLogValuesFunc, LogLatency, LogRequestID, LogStatus
Recoverпаника внутри цепочки: стек в лог, ошибка в c.Error -> 500StackSize, DisableStackAll, DisablePrintStack, LogLevel
RequestIDX-Request-Id: чужой из запроса или случайные 32 буквы; ставит заголовок до next(c)Generator, TargetHeader, RequestIDHandler
Timeout503 при превышении дедлайна; не останавливает обработчик; Deprecated в свежих v4Timeout, ErrorMessage
ContextTimeoutконтекст с дедлайном без гонок: DeadlineExceeded -> 503Timeout, ErrorHandler
Gzipсжатие ответа при Accept-Encoding: gzip, буфер с порогомLevel, MinLength
Decompressраспаковка сжатого тела запросаDecompressConfig
Staticфайлы из каталога: индекс, SPA-фоллбэк, листингRoot, Index, HTML5, Browse, IgnoreBase
CORSзаголовки cross-origin по списку источниковAllowOrigins, AllowMethods, AllowCredentials
CSRFтокен формы: кладёт его через c.Set("csrf", ...) — вы видели в уроке про c.SetTokenLookup, ContextKey
BasicAuthпара логин-пароль из Authorization, проверяет ваш ValidatorValidator, Realm
KeyAuthAPI-ключ в заголовке или queryKeyLookup, Validator
RateLimiterограничение частоты: считает запросы через ваш StoreStore, IdentifierExtractor
BodyLimitотсечка размера тела запросаLimit
Secureзащитные заголовки: XSS, nosniff, HSTSXSSProtection, HSTSMaxAge
Redirectредиректы http->https, www->апекс, слэшиHTTPSRedirect, WWWRedirect, TrailingSlash
MethodOverrideX-HTTP-Method-Override для форм без PUT/DELETEGetter
Proxyreverse proxy на пул бэкендов с балансировщикомTarget, Balancer

Соберите Logger со скипером health-чека

Заполните пропуск: доступ к запросу в скипере, фабрика логгера, тег времени и поле Skipper в конфиге.

// скипер: health-check не попадает в лог health := func(c echo.Context) bool { return c.().URL. == "/healthz" } e.Use(middleware.WithConfig(middleware.LoggerConfig{ Format: " ${status} ${method} ${uri} ${latency_human} ", CustomTimeFormat: "15:04:05.000", : health, }))

Соберите боевой минимум из каталога

Расставьте строки main() в правильном порядке: идентификатор запроса, логи, recovery, сжатие — и запуск. Помните правило прошлого урока: Logger раньше Recover.

Строки

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

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

Что дальше: свои слои поверх каталога#

Каталог закрывает типовые задачи, но все его слои — те же echo.MiddlewareFunc, что вы писали руками в прошлом уроке: конфиг, Skipper, фаза запроса до next(c), фаза ответа после. Соберите боевой минимум из order-задания, прогоните curl -i и найдите X-Request-Id в ответе и ${id} в строке лога — после этого встроенные middleware перестанут быть магией.

Следующий урок — про кастомные middleware: когда слой нужен свой — аутентификация по своим токенам, метрики, сквозные функции — и как оформить его фабрикой с конфигом, не повторив чужих граблей. А конвейер привязки и валидации из урока про bind и валидацию — готовый кандидат на собственный слой со скипером: ошибки валидации должны приходить к клиенту в едином формате, и оформление такого формата — тоже middleware. Навигация по курсу — на странице курса Echo.

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

Термин

middleware.Logger()

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

Формат — шаблон fasttemplate с тегами ${...}; дефолтный Format — однострочный JSON. ${id} читает X-Request-Id из запроса и ответа; ${bytes_in} — это Content-Length запроса, не размер тела. Скелет тот же, что у самодельного логгера: err = next(c), при ошибке c.Error(err). В v4.15 помечен Deprecated — новое API RequestLogger с флагами Log* и вашим LogValuesFunc.

1 / 6

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

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

Вывод

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

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

  • Пакет github.com/labstack/echo/v4/middleware — каталог готовых слоёв: Logger, Recover, RequestID, Timeout и ContextTimeout, Gzip, Static, CORS, CSRF, RateLimiter и другие; JWT вынесен в отдельный модуль labstack/echo-jwt.
  • Все встроенные middleware устроены одинаково: конфиг XConfig с полем Skipper, DefaultXConfig с дефолтами, фабрики X() и XWithConfig; внутри — те же фазы до next(c) и после, что у самодельного слоя.
  • echo logger: формат — шаблон fasttemplate с тегами ${...}, дефолт — JSON; свой формат задаётся в LoggerWithConfig; в v4.15 старый Logger помечен Deprecated, новое API — RequestLogger с LogValuesFunc.
  • echo recover: паника -> стек в лог и 500 через c.Error; StackSize 4 KB, LogLevel; http.ErrAbortHandler перевызывается и не подлежит перехвату.
  • RequestID переиспользует присланный X-Request-Id или генерирует 32 буквы через crypto/rand и ставит заголовок в ответ до next(c) — сквозная трассировка выходит сама собой.
  • Timeout (v4.2.0) отдаёт 503 сразу, но не останавливает обработчик и в свежих v4 Deprecated из-за гонок; замена — ContextTimeout на context.WithTimeout.
  • Gzip сжимает ответы с Accept-Encoding: gzip и уважает MinLength; Level: 0 неотличим от «не задано» и становится -1; Static умеет SPA-фоллбэк HTML5 и пропускает несуществующие файлы дальше по цепочке.

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

Соберите конвейер: RequestID, Logger с форматом "${time_custom} ${id} ${status} ${uri} ${latency_human}", Recover и Gzip с MinLength 512; добавьте скипер, пропускающий /healthz у всех слоёв сразу. Затем временно поставьте в обработчик panic("boom") и проверьте curl -i: клиент должен получить 500, а в выводе сервера — строку [PANIC RECOVER]. Уберите панику, отправьте запрос с заголовком -H "X-Request-Id: lesson-07" и найдите это значение и в ответе, и в логе. Наконец, сравните размеры ответа в curl и curl --compressed на JSON в несколько килобайт и объясните, зачем в таблице тегов у Logger-а есть bytes_in и bytes_out.

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

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

Как отключить встроенный middleware для отдельных путей?

Полем Skipper в конфиге: middleware.LoggerWithConfig(middleware.LoggerConfig{Skipper: func(c echo.Context) bool { return c.Request().URL.Path == "/healthz" }}). Первая строка каждого слоя проверяет Skipper и при true делает return next(c) без работы. Механизм общий: Skipper есть у Logger, Recover, Gzip, RequestID и остальных конфигов каталога; DefaultSkipper всегда возвращает false.

Что использовать вместо middleware.Timeout?

ContextTimeout: e.Use(middleware.ContextTimeout(30 * time.Second)). Он подменяет контекст запроса дедлайном через context.WithTimeout и c.SetRequest, не трогает response writer и не создаёт гонок, позиция в цепочке любая. Клиент получит 503, когда внутренняя операция вернёт context.DeadlineExceeded — для этого запросы к БД и HTTP-клиенты должны принимать ctx. Старый Timeout не останавливает обработчик и обязан стоять первым слоем.

Почему в JSON-логе Echo поле id пустое?

Тег ${id} берёт значение из заголовка X-Request-Id запроса, а при пустоте — из заголовка ответа. Пусто бывает, когда клиент не прислал свой идентификатор, а middleware.RequestID вообще не подключён. Добавьте e.Use(middleware.RequestID()) до остальных слоёв — каждая строка лога получит id: чужой или сгенерированный из 32 букв через crypto/rand.

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

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

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

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

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

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

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

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

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