Содержание урока · 10 разделов
- echo validation go: интерфейс из одного метода
- Конвейер bind → normalize → validate
- chi валидация: хелпер decodeAndValidate
- Gin, Echo, chi: кто и как валидирует
- 400 против 422: где ставить статус
- Единый хук: StructLevel и сообщения внутри Validate
- Чек-лист Echo и chi одним списком
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Gin из прошлого урока спрятал валидацию в один вызов и переключил словарь на binding. Echo и chi выбирают противоположную философию: ничего не прятать. Echo даёт точку подключения — интерфейс echo.Validator из одного метода — и честно оставляет вам решение, чем проверять; chi не даёт даже её, потому что он роутер поверх net/http и про тела запросов ничего не знает. Для validator v10 это означает приятную новость: здесь он работает без единой правки — родной словарь validate, родные ValidationErrors, родные структурные правила. Различие между фреймворками сводится к тому, кто и когда зовёт validate.Struct: в Echo — ваш Validation-хук из хендлера, в chi — ваш же код после декодера. Именно явность делает оба варианта прозрачнее gin-овского: когда запрос провален, вы точно видите строку, где это случилось.
echo validation go: интерфейс из одного метода#
Echo объявляет тип type Validator interface { Validate(i any) error } — один метод, и весь контракт: примите что угодно, верните ошибку, если данные плохи. В старых релизах v4 сигнатура встречается с interface{} — это тот же тип под старым именем, компилятор вашу реализацию примет в обоих написаниях. Реализация поверх validator v10 — считанные строки: структура с полем *validator.Validate и метод Validate, который вызывает Struct(i). Назначение — присваиванием: e.Validator = &CustomValidator{validate: validator.New()}, и с этого момента у каждого echo.Context есть рабочая c.Validate(i). Присваивайте сразу после e := echo.New(), до регистрации маршрутов: поле ничем не защищено, а влияние на живом сервере — гонка. Здесь же, в конструкторе валидатора, пропишите регистрации из десятого и одиннадцатого уроков — свои теги и структурные правила — один раз, при старте. Проверка структуры идёт по тегам validate: словарь никто не переключал, и структуры, наработанные в чистых v10-проектах, переезжают в Echo без правки. Контракт Validate(i) на вашей стороне: валидатор ждёт указатель на структуру — за ним идут правила; передать значение-не-структуру можно, но Struct ответит InvalidValidationError из пятого урока, и это ошибка программиста, а не данных. Удобный шаблон — отдельный пакет валидации: конструктор собирает validator.New(), выполняет регистрации и возвращает готовую реализацию echo.Validator; в main остаётся одна строка присваивания, а тесты собирают тот же пакет без HTTP.
Свой Validation-хук и хендлер на Echo
GOtype CustomValidator struct {
validate *validator.Validate
}
func (cv *CustomValidator) Validate(i any) error {
return cv.validate.Struct(i)
}
func main() {
e := echo.New()
e.Validator = &CustomValidator{validate: validator.New()}
// ...
}
func createTodo(c echo.Context) error {
var in TodoInput
if err := c.Bind(&in); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "битое тело")
}
if err := c.Validate(&in); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusCreated, in)
}CustomValidator реализует echo.Validator автоматически: компилятор сверит сигнатуру Validate. В хендлере — явные два шага: c.Bind раскладывает тело и вообще не смотрит на правила, c.Validate зовёт ваш хук. Ошибка валидации оборачивается в echo.NewHTTPError 400 — это *echo.HTTPError, и дефолтный обработчик доставит её клиенту.
Проверка: Validate без валидатора
e := echo.New() создан, но e.Validator никто не назначал. Что вернёт c.Validate(&in) на запросе?
Конвейер bind → normalize → validate#
Явность Echo расставляет роли по местам. c.Bind — раскладка: path-параметры, query, тело по Content-Type; на правила она не смотрит вовсе, и читаемый JSON с мусорными значениями развяжется без единой ошибки. c.Validate — правила: то, что вы назначили в e.Validator. Склейка — три строки вашего хендлера, и между ними есть место, которого нет в gin: нормализация. TrimSpace перед required, lower-case перед уникальностью — всё это делается между bind и validate, и конвейер bind → normalize → validate становится контрактом проекта: нормализуй, потом проверяй, потому что правила отработавшие на грязных данных бесполезны. Обратная сторона явности — забытый шаг: пропустили c.Validate — компилятор молчит, тест с валидным телом зелёный, а мусор едет в базу до первого ручного теста. Лечение дисциплинное: единый шаблон хендлера в команде, код-ревью на строку c.Validate и тест с битым телом на каждый эндпоинт — тесты правил из девятнадцатого урока здесь закрывают дыру. Ошибки двух шагов разводите с самого начала: c.Bind вернул ошибку — это проблемы транспорта, и сообщение не должно намекать на поля; c.Validate вернул ValidationErrors — это данные, и клиенту нужны детали по полям. Один и тот же echo.NewHTTPError(http.StatusBadRequest, err.Error()) на оба случая — самая частая небрежность Echo-проектов: она прячет тип ошибки и лишает фронтенд различия между «не смог прочитать» и «не прошёл проверку». Как именно устроена нормализация и почему её место именно там — отдельный сюжет пятнадцатого урока.
chi валидация: хелпер decodeAndValidate#
chi не навязывает даже bind: тело запроса читает кто угодно и как угодно, обычно json.NewDecoder прямо в хендлере. chi валидация поэтому выглядит как функция-хелпер: декодировали, проверили, вернули ошибку — и каждый хендлер зовёт её одной строкой. Тело http.Request — io.ReadCloser, который читается один раз, поэтому валидация не может жить в middleware до хендлера: middleware, прочитавший Body, оставит хендлер с пустым потоком. Декодирует тот, кто владеет данными, — хендлер, — и проверка структурой естественным образом живёт рядом. Ошибки — без сюрпризов: json-декодер даёт свои ошибки, validate.Struct — ValidationErrors, и errors.As разводит их на две ветки: детали по полям или честное «битое тело». Экземпляр валидатора — пакетная переменная или поле сервиса: ровно один на приложение, регистрации на старте. Перед декодом полезно ограничить размер тела: http.MaxBytesReader(w, r.Body, n) ставит потолок байт и возвращает ошибку при превышении — без него один гигантский POST способен занять память процесса. Обёртке нужен ResponseWriter, поэтому лимит ставят в начале хендлера; превышение приедет обычной ошибкой чтения в общую ветку «битое тело». Если хочется одинаковости с Echo, хелпер decodeAndValidate становится вашей реализацией того же Validation-хука — интерфейс чужой, идея общая.
chi: хелпер и хендлер
GOvar validate = validator.New()
func decodeAndValidate(r *http.Request, dst any) error {
if err := json.NewDecoder(r.Body).Decode(dst); err != nil {
return fmt.Errorf("битое тело: %w", err)
}
return validate.Struct(dst)
}
func createTask(w http.ResponseWriter, r *http.Request) {
var in TaskInput
if err := decodeAndValidate(r, &in); err != nil {
var verrs validator.ValidationErrors
if errors.As(err, &verrs) {
respondJSON(w, http.StatusBadRequest, fieldMap(verrs))
return
}
respondJSON(w, http.StatusBadRequest,
map[string]string{"error": err.Error()})
return
}
// in проверен
respondJSON(w, http.StatusCreated, in)
}Хелпер прячет две ветки источника: декодер и движок. Обёртка fmt.Errorf с %w сохраняет исходную ошибку декодера для логов. ValidationErrors приедут из validate.Struct и разойдутся errors.As-ом: поля — клиенту, прочее — общая фраза. Хендлер остаётся плоским: один вызов, две ветки, дальше бизнес.
Что уедет в базу
Прочитайте код и выберите, что он выведет:
func createTodo(c echo.Context) error {
var in TodoInput // Title string, validate:"required,max=140"
if err := c.Bind(&in); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "битое тело")
}
// строку c.Validate(&in) кто-то удалил при рефакторинге
return c.JSON(http.StatusCreated, in)
}
// тело запроса: {"title": ""}Gin, Echo, chi: кто и как валидирует#
| Фреймворк | Кто валидирует | Словарь тегов | Точка решения |
|---|---|---|---|
| Gin | встроенный движок за ShouldBind | binding | ваш хендлер ставит статус на err |
| Echo | ваш echo.Validator через c.Validate | validate | ваш хендлер оборачивает err в HTTPError |
| chi | ваш код после декодера | validate | ваш хелпер decodeAndValidate |
400 против 422: где ставить статус#
Явные конвейеры возвращают вам право выбора статуса, и тут же возникает вечный спор: 400 Bad Request или 422 Unprocessable Content. Логика 422 красивая: тело синтаксически разобралось, семантика провалилась. Но по факту большинство клиентского кода, документаций и прокси привыкло к 400, а детализация по полям и так отличает валидационный отказ от битого JSON. Историческая справка для спора: 422 придумали в WebDAV (RFC 4918) как Unprocessable Entity, позже её переварили общие спецификации HTTP — в RFC 9110 она называется Unprocessable Content. Статус законный, но не обязательный: 400 тоже покрывает синтаксически корректный, но бессмысленный запрос, так что выбор — конвенция команды, а не требование стандарта. Моё правило: выберите один статус для всех валидационных отказов проекта, пропишите его в константе и не спорьте второй раз — консистентность важнее каноничности. В Echo решение живёт в месте оборачивания ошибки — лучше всего прямо в CustomValidator, чтобы хендлеры вообще не думали о статусах; в chi — в хелпере; в gin — в respondFields. Единый формат ошибок API с кодами и деталями — тема двадцать четвёртого урока, а здесь зафиксируйте принцип: одна точка, где ValidationErrors становятся ответом, и один статус на проект.
Проверка: почему не middleware
Почему chi-валидацию не выносят в middleware до хендлера?
Рефакторинг main: e.Validator = ... перенесли в ветку, которая исполняется по флагу конфига, — и на продукте все POST-эндпоинты начали отвечать 500. В логе одна и та же строка: validator not registered; статус пятисотый, потому что ErrValidatorNotRegistered — не HTTPError, а дефолтный обработчик Echo считает такие ошибки аварией сервера. Тесты не поймали: они собирают приложение собственной функцией, где присваивание на месте. Мораль: присваивание e.Validator — строка уровня e := echo.New(), а smoke-тест на 201 с валидным телом обязан жить в CI.
Echo, chi и фоновые джобы должны делить один validator.New(): кэш структур, свои теги и StructLevel живут на экземпляре. Пакетная переменная или зависимость через конструктор — оба пути рабочие, лишь бы регистрации случались один раз на старте. Подробно о цене второго экземпляра — четырнадцатый урок.
Единый хук: StructLevel и сообщения внутри Validate#
Соберём три урока в одну точку. Ваш CustomValidator Validate не обязан быть однострочником: это законное место для нормализации перед проверкой, для StructPartial в PATCH-обновлениях, для конверта ошибок из каталога шестого урока. Структурные правила одиннадцатого урока регистрируются на том же экземпляре — и passwordmatch автоматически заработает в c.Validate без единого изменения хендлеров. Свои теги RegisterValidation — туда же. В chi всё это живёт в decodeAndValidate или в вашем типе с методом Validate, который хендлеры дергают после декодера: интерфейс другой, наполнение то же. Так проект получает то, что gin дал из коробки, но прозрачнее: bind и validate разделены, нормализация имеет своё место, а правила — одни и те же в HTTP-хендлерах, фоновых задачах и тестах. Сам хук легко тестировать без сервера: соберите CustomValidator с реальным движком и прогоните валидные и битые структуры через Validate — это обычные юнит-тесты; в httptest остаётся проверить, что хендлер правильно оборачивает ошибки. И да, это осознанный повтор формулы: одна библиотека, один экземпляр, один словарь тегов — три фреймворка перестают быть тремя мирами валидации.
Чек-лист Echo и chi одним списком#
- e.Validator присваивается сразу после echo.New(), до маршрутов
- c.Validate без назначенного валидатора — ErrValidatorNotRegistered и 500, а не 400
- c.Bind раскладку делает, правила не проверяет: явный вызов c.Validate обязателен
- конвейер хендлера: bind → normalize → validate, порядок — часть контракта
- chi: json.NewDecoder в хендлере или хелпер decodeAndValidate, middleware до декодера бессилен
- ошибки: ValidationErrors через errors.As, остальное — битое тело без деталей
- статус валидационного отказа — одна точка решения, один на проект
- в хук же: нормализация, StructPartial, каталог сообщений и регистрации на старте
Оба конвейера — Echo и chi — проверяются без прослушивания сокета: httptest.NewServer или вызов ServeHTTP на голом роутере дают реальный биндинг и декод, а валидатор в них тот же. Нормализацию и StructLevel тестируйте и вовсе напрямую — они не знают про HTTP. Единственное, что не поймать юнит-тестом, — что e.Validator вообще назначен: для этого и живёт smoke-тест на реальном экземпляре приложения.
Соберите подключение к Echo
Четыре пропуска: поле, метод интерфейса, вызов движка и статус ошибки.
Соберите конвейер Echo
POST /todos с битым заголовком. Расставьте шаги по порядку.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: Echo и chi
Следующий урок — один экземпляр на приложение: что именно кэширует validator.New(), почему регистрации не потокобезопасны и во что обходится второй экземпляр. Механика ShouldBind из gin-мира — в двенадцатом уроке, сравнение конвейеров — в курсе Echo и курсе chi. Примеры урока запускаются локально: Echo, chi и validator — внешние модули, песочница сайта работает только с чистым Go, поэтому конвейер гоняйте go run-ом с httptest или curl-ом.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Echo даёт интерфейс echo.Validator из одного метода Validate; реализация поверх v10 назначается e.Validator на старте.
- c.Validate без валидатора возвращает ErrValidatorNotRegistered — дефолтный обработчик отвечает 500, а не 400.
- c.Bind не валидирует: явный конвейер bind → normalize → validate — контракт каждого хендлера.
- chi — роутер: декодирует хендлер, поэтому валидация — хелпер decodeAndValidate рядом с декодером, не middleware.
- Один экземпляр v10 на приложение: свои теги, StructLevel и каталог сообщений живут в вашем Validation-хуке.
- Статус валидационного отказа — одна точка решения: в хуке или хелпере, один на проект.
- Словарь здесь родной validate: структуры из чистых v10-проектов переезжают без правки тегов.
Домашнее задание
Поднимите мини-сервис на Echo и тот же на chi: POST /tasks со структурой Title validate:"required,min=3" и Priority validate:"required,gte=1,lte=5". В Echo напишите CustomValidator с нормализацией заголовка внутри Validate, в chi — хелпер decodeAndValidate. Добавьте smoke-тест на 201 и тест на пустой title с ожиданием 400, затем удалите строку c.Validate и убедитесь, что тест поймал регресс. Сверьте, что оба сервиса отдают одинаковый формат ошибок. Затем поставьте http.MaxBytesReader с лимитом в один мегабайт и прогоните тест с телом вдвое больше: убедитесь, что ответ — честный 400, а не зависание или 500.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему Echo не встраивает валидатор, как Gin?
Это осознанный минимализм: Echo даёт точку подключения — интерфейс из одного метода — и не хочет решать за вас, чем проверять. Стоимость — явный вызов c.Validate и назначение e.Validator; выигрыш — свой словарь тегов, своя нормализация и свой конверт ошибок без борьбы с чужими умолчаниями. Из того же решения растёт и цена: Echo не может гарантировать, что валидация вообще случилась, — гарантия на совести команды, и smoke-тест из живого места здесь не перестраховка, а минимум.
Можно ли провалидировать тело в Echo-биндере, чтобы не звать c.Validate?
Можно написать свой Binder, который после раскладки вызывает Validate, — Echo позволяет подменить и его. Но это снова прятание конвейера: код-ревью сложнее увидеть пропущенную проверку, а нормализация теряет своё место. Практика устойчивее: явные три строки bind, normalize, validate в каждом хендлере.
Как в chi разделить ошибки декодера и валидации?
По типу: json.NewDecoder вернёт свои ошибки (синтаксис, типы), validate.Struct — validator.ValidationErrors. В decodeAndValidate поставьте errors.As: ветка ValidationErrors отдаёт детали по полям, остальное — общая фраза «битое тело». Обёртка с %w сохранит исходную ошибку декодера для логов. Полезный второй слой — журнал: полное err.Error() от ValidationErrors в лог сервера и короткий конверт клиенту; связка выручает, когда через полгода приходит тикет «не принимает форму», а воспроизвести точное тело уже поздно.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Asynq в Go: очередь фоновых задач на Redis
Курс asynq для Go: клиенты и воркеры, отложенные и периодические задачи, ретраи, очереди с приоритетами, агрегация, Inspector и asynqmon. 25 уроков.
- Строить клиент и воркер asynq и подключать Redis
- Отправлять отложенные, периодические и уникальные задачи
- Настраивать ретраи, таймауты и приоритеты очередей
- Мониторить задачи через Inspector и asynqmon