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

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

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

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

ValidationErrors: что именно сломалось

После провала правил validate.Struct возвращает не абстрактную ошибку, а структуру с полным описанием: validationerrors go хранит имя поля, споткнувшийся тег и его параметр — достаточно, чтобы ответить клиенту по-человечески. В этом уроке разбираем ValidationErrors и FieldError по методам: errors.As, Field(), Tag(), Param(), Value(), Namespace() — и собираем понятный ответ 400 вместо английской простыни Key: '...'.

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

Вот эту строку вы видели в уроке про первый запуск: Key: 'SignupForm.Password' Error:Field validation for 'Password' failed on the 'min' tag. Английская, подробная и совершенно непригодная для пользователя — но присмотритесь, сколько в ней информации: имя структуры, имя поля, провалившийся тег. Всё это валидатор складывает в структуру ошибки, и сегодня мы научимся доставать части по отдельности: тип ValidationErrors, каждый его элемент FieldError, методы Field, Tag, Param и компания. К концу урока простыня Key: '...' превратится в сырьё для понятных ответов 400, а полная система сообщений на русском станет темой шестого урока. Умение читать структуру ошибки — это навык, который отделяет валидацию «как в туториале» от валидации, за которую не стыдно на ревью.

validationerrors go: два типа ошибок из Struct#

err, который возвращает validate.Struct, — один из двух типов, и различать их нужно на уровне кода. Первый — validator.ValidationErrors: срез FieldError, по записи на каждое провалившееся поле; это нормальный рабочий сценарий — данные не прошли правила, отвечаем 400 по полям. Второй — *validator.InvalidValidationError: валидатору передали не структуру — nil, число, срез; это ошибка программиста, и её место в логах или панике на старте, а не в ответе клиенту. Оба типа — конкретные значения, реализующие error, поэтому достают их через errors.As с указателем на нужный тип: это безопаснее прямого приведения типов, потому что err может быть завёрнут другим слоем. Порядок веток имеет значение по соображениям читаемости: сначала узкая InvalidValidationError, потом широкая ValidationErrors — формально они не пересекаются, но такой порядок читается как чек-лист: баг кода, отказ данных, всё остальное. Стандартный паттерн — в блоке ниже.

Две ветки errors.As — канонический разбор

GO
err := validate.Struct(&form)
if err != nil {
        var invalid *validator.InvalidValidationError
        if errors.As(err, &invalid) {
                // не структура: баг кода, не данные
                panic(fmt.Sprintf("validate.Struct: %v", invalid))
        }

        var verrs validator.ValidationErrors
        if errors.As(err, &verrs) {
                for _, e := range verrs {
                        fmt.Println(e.Field(), e.Tag(), e.Param())
                }
                // Password min 8
                return
        }
        // всё остальное — неожиданно, общий обработчик
}

Две ветки errors.As по типам: InvalidValidationError — не-структура на входе, ValidationErrors — провал правил. В цикле по verrs каждая запись отдаёт имя поля, тег и параметр тремя методами; для Password с тегом min=8 цикл напечатает Password min 8. Всё остальное в err считайте неожиданным и уходите в общий обработчик ошибок.

validator field tag: методы FieldError#

FieldError — маленькая, но говорящая структура. Field() возвращает имя поля структуры: Password; для элементов среза после dive — имя с индексом, вроде Tags[2]. Tag() — тег, на котором проверка споткнулась: required, min, email. Param() — параметр этого тега строкой: «8» для min=8, «admin user» для oneof, пустая строка для required — у него параметров нет. Value() — само значение поля, которое не прошло: удобно для логов, но не для ответа клиенту. Namespace() — полный путь с точками: SignupForm.Address.City; он оживёт в уроке про вложенные структуры, а пока запомните сам факт. ActualTag() возвращает реальный тег без алиасов — на простых тегах совпадает с Tag(), разница появляется с RegisterAlias, когда одному правилу придумывают своё имя. Kind() и Type() — reflect-категория и тип поля, если хочется ветвиться по ним: строка это или число, int или time.Time; так, каталог сообщений может для числовых полей говорить «минимум N», а для строковых — «минимум N символов», не заводя отдельные теги. И метод Error(), который собирает ту самую простыню Key: '...' — теперь понятно, из каких частей. Для сообщений на русском нужны первые три — Field, Tag и Param — плюс словарь; остальное пригодится в специфических сценариях. И ещё разница, которую полезно понимать заранее: Field() и Namespace() отдают имена по настоящим полям структуры, без переименований по json-меткам — валидатор про сериализацию не знает, у него свой взгляд на имена; будем помнить при маппинге.

Методы FieldError на одном примере#

МетодЧто возвращаетПример для Password при min=8
Field()имя поля структурыPassword
Tag()тег, на котором провалилась проверкаmin
Param()параметр тега строкой8
Value()значение поля, которое не прошлоabc
Namespace()полный путь через точкиSignupForm.Password
ActualTag()тег без алиасовmin
Error()собранная строка Key: '...' Error:...Key: 'SignupForm.Password' Error:...

Проверка: Param у oneof

Role string с тегом oneof=admin user получил значение root. Что вернёт e.Param()?

Первый провалившийся тег побеждает#

Важное свойство исполнения: поле проверяется по правилам слева направо, и первый провалившийся тег завершает проверку этого поля. Password string с тегом required,min=8 и значением «» вернёт одну запись с тегом required — до min дело не дойдёт, пустая строка уже провалила первый тег. Из этого два следствия. Первое: не рассчитывайте увидеть в ValidationErrors все причины по одному полю — полный список причин по полю валидатор не соберёт; если бизнесу нужен пошаговый диалог, проверяйте повторно после каждого исправления или делите форму на шаги, где каждая валидация отвечает только про текущий шаг. Второе: порядок правил в теге управляет тем, какое сообщение получит пользователь: required первым даёт «укажите поле», а min первым — «слишком коротко» даже для пустой строки. В курсе мы ставим required первым — это договорённость между тегом и текстом подсказки, которая делает сообщения предсказуемыми. На практике из этого свойства вырастает целая дисциплина UX: длинная форма с десятью ошибками не должна возвращать все десять, если девять из них — следствия одной пустой строки; пошаговые формы с валидацией текущего шага дают клиенту короткий список и держат сообщения осмысленными. А если продукт требует показать все проблемы разом, собирайте их отдельным проходом по полям — правила слева направо это механика валидатора, а не закон интерфейса. При этом сами поля независимы: три сломанных поля дадут три записи, и клиент получит весь список разом — это и есть преимущество собирающей валидации перед стоп-на-первой-ошибке. И последнее свойство, которое стоит прочувствовать: порядок записей в срезе следует порядку полей структуры — Password раньше Age. Не полагайтесь на это в логике ответа, но для чтения логов знание полезное: запись с незнакомым полем значит, что смотрят не ту версию структуры.

Сколько записей и какие теги

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

type Profile struct {
        Password string `validate:"required,min=8"`
        Age      int    `validate:"gte=0"`
}

p := Profile{Password: "", Age: -1}
err := validate.Struct(&p)
// сколько записей в ValidationErrors и какие теги?

Мини-каталог: от тега к тексту

GO
func fieldMessage(e validator.FieldError) string {
        switch e.Tag() {
        case "required":
                return "обязательное поле"
        case "min":
                return fmt.Sprintf("минимум %s символов", e.Param())
        case "max":
                return fmt.Sprintf("максимум %s символов", e.Param())
        case "email":
                return "укажите адрес почты"
        default:
                return e.Field() + ": проверка " + e.ActualTag() + " не прошла"
        }
}

Мини-каталог: текст подбирается по Tag(), параметр тега подставляется в сообщение — минимум 8 символов. Ветка default с ActualTag() — страховка: незнакомый тег не вернёт пустоту, а даст честную техническую строку, по которой вы узнаете о пропущенном кейсе. Полная система с русскими текстами и уточнениями по полям — тема шестого урока.

InvalidValidationError: когда сломался код#

*InvalidValidationError заслуживает отдельного блока, потому что его появление почти всегда означает баг в вашем коде, а не в данных. Валидатор возвращает его, когда в Struct передали nil, не-указатель, указатель на не-структуру. Типичные сценарии из жизни: функция принимает any и доходит до validate.Struct(data) с числом внутри; тест передаёт nil «а вдруг сработает»; рефакторинг сменил сигнатуру, и вместо структуры поехала ошибка предыдущего шага. Обращение однообразное: это не пользовательская история — логируйте с контекстом или паникуйте на старте, чтобы баг всплыл при первом же прогоне, а не жил годами в тёмной ветке. Практика с паникой на старте выглядит жёстко, но у неё есть обоснование: InvalidValidationError возможен только там, где потерян тип данных — после рефакторинга или в generic-обёртке; такие места всплывают на первом интеграционном тесте, и лучше упасть в тестовом окружении с внятной причиной, чем молча пропускать проверку в проде. Если паника в вашем сервисе недопустима, логируйте на уровне error и возвращайте 500 — главное, не превращайте её в 400: пользователь не виноват в том, что код передал валидатору не структуру. Отсюда же правило для обёрток: функция-помощник validateOf(data any) error обязана различать оба типа и документировать, что InvalidValidationError наружу не отдаётся. В тестах правил — отдельный урок про тесты — InvalidValidationError проверяют явно: подают nil и ожидают именно этот тип, а не ValidationErrors; это фиксирует контракт проверки в коде.

Проверка: что вернёт Error()

Что вернёт err.Error() у ValidationErrors с тремя записями?

От простыни к конверту 400#

Соберём практику урока в принципы ответа клиенту. Не отдавайте err.Error() как есть: английская простыня, утечка внутренних имён полей и структуры — и всё это вместо двух слов по-русски. Не отдавайте и Value() провалившихся значений: в ошибке уедет то, что ввёл пользователь, включая пароли и паспорта; значение — для логов, и не для всех полей. Отвечайте списком Details: по записи на каждое сломанное поле, собранной из Field, Tag, Param и словаря сообщений, — так фронтенд подсвечивает инпуты один к одному. Этот конверт — тема двух уроков: шестой соберёт каталог русских текстов, двадцать четвёртый оформит единый формат ошибок API с кодами и полем. А если вы работаете в Gin, тот же паттерн уже разобран в двенадцатом уроке — с тегами binding и ShouldBindJSON; механика FieldError при этом идентична. Практическая проверка принципов простая: откройте любой свой ответ 400 и посчитайте, сколько в нём утечёк из прошлого — технических имён, английских тегов, значений полей. Каждая утечка — это ровно то, что мы сегодня разложили по методам.

Что вытащить из FieldError для ответа#

  • Field() — имя поля для маппинга в JSON-имя и подсветки инпута
  • Tag() — ключ каталога сообщений: один текст на все поля с этим тегом
  • Param() — параметр тега: граница диапазона или перечень oneof подставляются в текст
  • Value() — провалившееся значение: в лог, но не в ответ клиенту
  • Namespace() — полный путь через точки: понадобится во вложенных структурах

Соберите разбор ошибок

Восстановите тип ошибки, errors.As и два метода FieldError — четыре пропуска.

var verrs validator. if errors.(err, &verrs) { for _, e := range verrs { switch e.() { case "min": fmt.Printf("минимум %s символов", e.()) } } }

Соберите путь ошибки в ответ 400

От err до Details в ответе — расставьте шаги по порядку.

Строки

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

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

Карточки: анатомия ошибки

Термин

ValidationErrors

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

Срез FieldError, по записи на каждое провалившееся поле. Реализует error; Error() склеивает записи переводами строк.

1 / 6

Шестой урок — русские сообщения: двухуровневый каталог, уточнения по полям и согласование по числам. Двенадцатый — тот же механизм в Gin с тегами binding и ShouldBindJSON. Двадцать четвёртый — единый формат ошибок API с кодами и Details. А соседний разбор из курса Gin — валидация в Gin — покажет, как тот же FieldError живёт внутри фреймворка и собирает Envelope-конверт с кодом VALIDATION_FAILED: синтаксис там отличается только словарём тегов.

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

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

Вывод

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

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

  • Struct возвращает один из двух типов: ValidationErrors — отказ правил, InvalidValidationError — не-структура на входе.
  • Оба типа достаются errors.As с указателем на тип; сначала узкая ветка про баг кода, потом рабочая про данные.
  • Field() — имя поля структуры, Tag() — споткнувшийся тег, Param() — его параметр строкой; этого хватает для каталога сообщений.
  • Первый провалившийся тег завершает проверку поля: Password с «» даст required, а не min.
  • Поля независимы: три сломанных поля — три записи в ValidationErrors, клиент получает список разом.
  • Field() — не JSON-имя: для подсветки полей на фронте нужен маппинг или Namespace().
  • err.Error() — для лога; клиенту едут Details из каталога сообщений, Value() наружу не отдаётся.

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

Возьмите форму из прошлых уроков с тремя полями и сломайте её двумя способами: пустыми значениями и значениями вне диапазона. Напечатайте для каждого случая все пять методов FieldError — Field, Tag, Param, Value, Namespace — и убедитесь, что у Password с пустой строкой тег required, а не min. Потом напишите fieldMessage с ветками на required, min и max и соберите ответ, где для каждой записи есть русское сообщение.

Личные заметкиПусто
Хранятся только в вашем браузере (Validator · ValidationErrors: что именно сломалось)

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

Почему в ошибке тег required, хотя в поле стоит и min?

Поле проверяется по правилам слева направо до первого провала. Пустая строка провалила required, и до min дело не дошло. Исправьте required — следующая проверка ответит уже про min. Это же свойство используют тесты: подавая заведомо пустые данные, ожидайте required, а не диапазон, — так контракт правил фиксируется в коде.

Как отличить ошибку данных от бага кода?

По типу: validator.ValidationErrors — нормальный отказ правил, данные не прошли; *validator.InvalidValidationError — валидатору передали не структуру, это баг. Разводите их двумя ветками errors.As и обрабатывайте по-разному.

Можно ли отправлять клиенту Value() провалившегося поля?

Не стоит: в значении может быть пароль, телефон или что-то конфиденциальное. Value() — инструмент для логов, причём не для всех полей; клиенту едут Field, Tag и Param, из которых собираются понятные сообщения без утечек.

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

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

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

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

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

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

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