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

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

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

Урок 13 из 25 ~18 мин +20 XP

Echo, chi и свой Validation-хук

Echo validation go начинается с честного признания фреймворка: валидатора в коробке нет — есть интерфейс из одного метода, который вы наполняете сами, а chi валидация и вовсе начинается после декодера, потому что chi это роутер. Подключим validator v10 к обоим, соберём явный конвейер bind → normalize → validate и сделаем свой Validation-хук, в котором живут нормализация, StructLevel и конверт ошибок.

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

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

GO
type 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: хелпер и хендлер

GO
var 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встроенный движок за ShouldBindbindingваш хендлер ставит статус на err
Echoваш echo.Validator через c.Validatevalidateваш хендлер оборачивает 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 до хендлера?

Единый хук: 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

Четыре пропуска: поле, метод интерфейса, вызов движка и статус ошибки.

type CustomValidator struct { validate *validator.Validate } func (cv *CustomValidator) (i any) error { return cv.validate.(i) } // в main: e. = &CustomValidator{validate: validator.New()} // в хендлере: if err := c.Validate(&in); err != nil { return echo.NewHTTPError(http., "проверьте поля") }

Соберите конвейер Echo

POST /todos с битым заголовком. Расставьте шаги по порядку.

Строки

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

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

Карточки: Echo и chi

Термин

echo.Validator

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

Интерфейс из одного метода Validate(i any) error. Назначается присваиванием e.Validator; реализация поверх v10 — метод, зовущий validate.Struct.

1 / 6

Следующий урок — один экземпляр на приложение: что именно кэширует 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.

Личные заметкиПусто
Хранятся только в вашем браузере (Validator · Echo, chi и свой Validation-хук)

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

Почему 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 в лог сервера и короткий конверт клиенту; связка выручает, когда через полгода приходит тикет «не принимает форму», а воспроизвести точное тело уже поздно.

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

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

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

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

Asynq в Go: очередь фоновых задач на Redis

Курс asynq для Go: клиенты и воркеры, отложенные и периодические задачи, ретраи, очереди с приоритетами, агрегация, Inspector и asynqmon. 25 уроков.

  • Строить клиент и воркер asynq и подключать Redis
  • Отправлять отложенные, периодические и уникальные задачи
  • Настраивать ретраи, таймауты и приоритеты очередей
  • Мониторить задачи через Inspector и asynqmon