Содержание урока · 13 разделов
- echo logger: каталог штатных middleware
- echo logger: анатомия формата Logger
- echo recover: конфиг, стек и LogLevel
- RequestID: сквозной идентификатор запроса
- Skipper: один механизм выключения для всех
- Timeout: что случилось с middleware.Timeout
- Gzip: сжатие с буфером и порогом
- Static: файлы, SPA-фоллбэк и Browse
- Каталог пакета middleware Echo v4
- Что дальше: свои слои поверх каталога
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
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 — общий для всех конфигов — покажем как штатный способ выключать слой для отдельных путей.
Минимум, который стоит ставить всегда
GOpackage 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/pingSkipper: один механизм выключения для всех#
Каждый конфиг каталога начинается с одного и того же поля: 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 секунды. Что увидит клиент и что станет с обработчиком?
Файл timeout.go начинается с многострочного WARNING: этот middleware «causes more problems than it solves», обязан быть первым в цепочке и способен дать data race на response writer, если стоит не первым. Тот же файл показывает честную альтернативу — свой слой на context.WithTimeout плюс c.SetRequest: это и есть ContextTimeout внутри. Пример миграции из доккомментария: было e.Use(middleware.Timeout()) — стало e.Use(middleware.ContextTimeout(30 * time.Second)). В существующем коде пометьте миграцию в бэклоге: старый слой работает, но с известными граблями.
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.Writer | Format, CustomTimeFormat, CustomTagFunc, Output |
| RequestLogger | тот же лог новым API: поля включаются флагами Log*, значения собирает ваш LogValuesFunc | LogValuesFunc, LogLatency, LogRequestID, LogStatus |
| Recover | паника внутри цепочки: стек в лог, ошибка в c.Error -> 500 | StackSize, DisableStackAll, DisablePrintStack, LogLevel |
| RequestID | X-Request-Id: чужой из запроса или случайные 32 буквы; ставит заголовок до next(c) | Generator, TargetHeader, RequestIDHandler |
| Timeout | 503 при превышении дедлайна; не останавливает обработчик; Deprecated в свежих v4 | Timeout, ErrorMessage |
| ContextTimeout | контекст с дедлайном без гонок: DeadlineExceeded -> 503 | Timeout, 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.Set | TokenLookup, ContextKey |
| BasicAuth | пара логин-пароль из Authorization, проверяет ваш Validator | Validator, Realm |
| KeyAuth | API-ключ в заголовке или query | KeyLookup, Validator |
| RateLimiter | ограничение частоты: считает запросы через ваш Store | Store, IdentifierExtractor |
| BodyLimit | отсечка размера тела запроса | Limit |
| Secure | защитные заголовки: XSS, nosniff, HSTS | XSSProtection, HSTSMaxAge |
| Redirect | редиректы http->https, www->апекс, слэши | HTTPSRedirect, WWWRedirect, TrailingSlash |
| MethodOverride | X-HTTP-Method-Override для форм без PUT/DELETE | Getter |
| Proxy | reverse proxy на пул бэкендов с балансировщиком | Target, Balancer |
Тег ${custom} вызывает CustomTagFunc func(c echo.Context, buf *bytes.Buffer) (int, error). Внутри доступен контекст: достаньте username, который authStub из прошлого урока положил через c.Set("username", user), и допишите его в буфер. Учтите две вещи: если CustomTagFunc не задана, тег ${custom} просто исчезает из строки лога (проверка на nil внутри Logger-а), а в JSON-формате вы пишете сырой кусок строки — кавычки и экранирование на вашей совести, Echo проверит валидность только базово.
Соберите Logger со скипером health-чека
Заполните пропуск: доступ к запросу в скипере, фабрика логгера, тег времени и поле Skipper в конфиге.
Соберите боевой минимум из каталога
Расставьте строки main() в правильном порядке: идентификатор запроса, логи, recovery, сжатие — и запуск. Помните правило прошлого урока: Logger раньше Recover.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Что дальше: свои слои поверх каталога#
Каталог закрывает типовые задачи, но все его слои — те же echo.MiddlewareFunc, что вы писали руками в прошлом уроке: конфиг, Skipper, фаза запроса до next(c), фаза ответа после. Соберите боевой минимум из order-задания, прогоните curl -i и найдите X-Request-Id в ответе и ${id} в строке лога — после этого встроенные middleware перестанут быть магией.
Следующий урок — про кастомные middleware: когда слой нужен свой — аутентификация по своим токенам, метрики, сквозные функции — и как оформить его фабрикой с конфигом, не повторив чужих граблей. А конвейер привязки и валидации из урока про bind и валидацию — готовый кандидат на собственный слой со скипером: ошибки валидации должны приходить к клиенту в едином формате, и оформление такого формата — тоже middleware. Навигация по курсу — на странице курса Echo.
Карточки: встроенные middleware
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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.
Личные заметкиПусто
Вопросы и ответы по уроку
Как отключить встроенный 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.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
GORM: ORM-библиотека для Go
Курс GORM: модели, CRUD, ассоциации, транзакции, хуки, пагинация, оптимизация запросов. 25 уроков с примерами и проектом слоя данных.
- Подключать БД и настраивать пул соединений
- Описывать модели и связи: 1:1, 1:N, M:N
- Писать CRUD и сложные выборки chainable-стилем
- Контролировать N+1 через Preload