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

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

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

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

StructLevel: правила на всю структуру

StructLevel validation — это RegisterStructValidation: функция, которую валидатор вызывает для структуры целиком, когда правило не живёт в теге одного поля. В этом уроке пишем кросс-польные проверки — совпадение пароля с подтверждением, сумму позиций против итога, длительность события — и разбираем StructLevel: sl.Current(), ReportError с пятью аргументами и порядок, в котором структурные ошибки попадают в общий список.

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

К десятому уроку у вас на вооружении теги, условные правила и собственные функции-валидаторы, но все они смотрят на одно поле. Правило «сумма позиций заказа равна полю Total» смотрит на N полей сразу, «событие длится минимум три дня» — на два поля и арифметику, «из трёх контактов заполнен хотя бы один» — на три. Частично такие случаи закрывали eqfield и required_without_all, но у них есть потолок: никаких вычислений, никаких агрегатов и сообщение с чужим тегом. Для правил на всю структуру у валидатора есть отдельный уровень — RegisterStructValidation: вы регистрируете функцию на типе, движок вызывает её после обхода полей, а ошибки из неё едут в тот же ValidationErrors, что и обычные. Имя тега вы придумываете сами — это правило с вашим названием, а не «провал на eqfield».

structlevel validation: функция на тип#

Регистрация — метод экземпляра: validate.RegisterStructValidation(fn, MyType{}), где fn — функция типа validator.StructLevelFunc, то есть func(sl validator.StructLevel). Типы после функции — вариадические: столько структур, сколько нужно, и указатель не обязателен — валидатор разыменует его сам через reflect.Indirect, поэтому канон из официальных примеров — RegisterStructValidation(fn, User{}) с не-указательным значением. Регистрируйте на старте, рядом с RegisterValidation и RegisterAlias: метод не потокобезопасен, как и все регистрации, — та же дисциплина второго урока. Момент вызова важен и приятен: structlevel validation срабатывает ПОСЛЕ того, как отработали все теги полей структуры, — глубинный обход доходит до конца, и тогда движок зовёт вашу функцию. Из этого следует поведение всего списка ошибок: записи от полей ложатся в ValidationErrors первыми, в порядке полей, а за ними — всё, что вы нарапортовали из структурной функции, в порядке вызовов ReportError. Поля и структура работают в один проход: три сломанных тега и одно структурное правило дадут четыре записи в одном err, и клиент получит полный список разом — никакого второго вызова validate.Struct не нужно. А если структура вложенная, функция зовётся каждый раз, когда обход доходит до экземпляра этого типа: правило на Address сработает для каждого адреса в срезе.

Пароль и подтверждение: правило с собственным именем

GO
type RegistrationForm struct {
        Password        string `validate:"required,min=8"`
        ConfirmPassword string `validate:"required"`
}

var validate = validator.New()

func init() {
        validate.RegisterStructValidation(RegistrationStructLevel, RegistrationForm{})
}

func RegistrationStructLevel(sl validator.StructLevel) {
        form := sl.Current().Interface().(RegistrationForm)

        if form.Password != form.ConfirmPassword {
                sl.ReportError(
                        form.ConfirmPassword,
                        "ConfirmPassword", "ConfirmPassword",
                        "passwordmatch", "Password",
                )
        }
}

Регистрация на типе в init — до первых проверок, как требует контракт регистраций. sl.Current() возвращает reflect.Value текущей структуры, Interface() превращает его в RegistrationForm, и дальше это обычный Go: сравнили два поля. ReportError получает значение, два имени поля, выдуманный тег passwordmatch и параметр-подсказку Password. Провал приедет в ValidationErrors с Tag() равным passwordmatch.

Проверка: когда зовут структурную функцию

У формы три поля, одно провалило тег min. Что произойдёт с зарегистрированной структурной функцией?

sl.Current() и ReportError: пять аргументов#

StructLevel — интерфейс с семью методами, но в ежедневной работе живут три. Current() — reflect.Value той самой структуры, для которой вызвана функция: через него читаются поля. Top() — корневая структура вызова: для вложенных правил видно, кто приехал сверху, — на глубокой вложенности путь к корню собирается через Parent(), родителя текущего уровня. Validator() возвращает сам экземпляр *Validate — из функции можно позвать validate.Var или свои теги, не замыкаясь на глобальную переменную. Чтение полей — на выбор: Interface().(RegistrationForm) с приведением типа или sl.Current().FieldByName("Password").String() по имени — первый путь даёт читаемый код, второй не зависит от типа и не паникует с лишними полями. ReportError принимает пять аргументов, и раскладка такая: field — само значение, fieldName — имя, которое попадёт в Namespace() и Field() ошибки, structFieldName — имя из объявления структуры, tag — ваш тег (можно выдуманный с нуля, можно существующий), param — строка-параметр, пустая или подсказка вроде имени пары-поля. Без RegisterTagNameFunc оба имени совпадают; с ним fieldName — альтернативное, например json-имя. Запомните: ReportError ничего не проверяет — он только дописывает готовую запись об ошибке; вся логика решения — ваша. Оставшиеся методы не для красоты. Validator() открывает дверь к обычному API экземпляра: посчитанную внутри правила сумму можно прогнать через sl.Validator().Var(reflect.ValueOf(sum), "required,min=1") — словарь тегов тот же, что и у полей. А ReportValidationErrors прикрепляет готовый срез ValidationErrors с префиксом пути: пригодится, когда внутри структурной функции вы проверили вложенную структуру отдельным вызовом и хотите, чтобы её записи легли в общий список с правильным Namespace(), а не болтались отдельной ошибкой. В ежедневной работе почти всё закрывает ReportError, оба метода ждут своего часа — но знать о них стоит до того, как придётся выдумывать обходные пути.

Методы StructLevel#

МетодЧто возвращает
Current()reflect.Value текущей структуры — главный вход к полям
Top()reflect.Value корневой структуры вызова Struct()
Parent()родительская структура текущего уровня
Validator()сам экземпляр *Validate — можно звать Var и свои теги
ExtractType(field)разыменованное значение, Kind и флаг nullable
ReportError(field, fieldName, structFieldName, tag, param)дописать FieldError с вашими именами и тегом
ReportValidationErrors(relNs, relStructNs, errs)прикрепить готовые ValidationErrors с префиксом пути

validator structlevel: сумма позиций и длительность#

Разберём два правила, для которых тегов не существует. Первое — заказ: Items []Item и Total int64; правило «сумма цен равна итогу» агрегирует срез, и ни один тег поля это не выразит, потому что смотреть нужно на все элементы сразу. Второе — событие: Starts и Ends time.Time; правило «конференция длится минимум три дня» — это не просто сравнение полей, а арифметика с AddDate, и gtfield здесь бессилен. Оба правила — обычный Go внутри структурной функции: посчитали, сравнили, нарапортовали. Заметьте, как выбирается имя тега: summatch и minduration — это названия бизнес-инвариантов, и именно они появятся в FieldError.Tag(). Не называйте правило именем встроенного тега: если summatch однажды переедет в RegisterValidation как настоящий тег, сообщения и тесты останутся совместимыми, а вот путаницы «тот же Tag() — разная механика» лучше избегать с первого дня. И держите функцию структурного уровня такой же чистой, как теговые функции из десятого урока: чтение полей, вычисление, ReportError — без сети, без времени, без изменения данных.

Заказ и событие: два правила

GO
func OrderStructLevel(sl validator.StructLevel) {
        order := sl.Current().Interface().(Order)

        var sum int64
        for _, it := range order.Items {
                sum += it.Price
        }
        if sum != order.Total {
                sl.ReportError(order.Total, "Total", "Total", "summatch", "")
        }
}

func EventStructLevel(sl validator.StructLevel) {
        cur := sl.Current()
        starts := cur.FieldByName("Starts").Interface().(time.Time)
        ends := cur.FieldByName("Ends").Interface().(time.Time)

        if ends.Before(starts.AddDate(0, 0, 3)) {
                sl.ReportError(ends, "Ends", "Ends", "minduration", "3d")
        }
}

validate.RegisterStructValidation(OrderStructLevel, Order{})
validate.RegisterStructValidation(EventStructLevel, Event{})

Две функции — два стиля чтения. Заказ разбирается одним Interface().(Order): срез, цикл, накопление. Событие читает поля по именам через FieldByName — удобно, когда правил немного и заведение переменной не окупается. Minduration показывает параметр в действии: строка «3d» приедет в e.Param(), и сообщение «минимум %s» соберётся без констант в тексте.

Что окажется в ValidationErrors

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

type RegistrationForm struct {
        Password        string `validate:"required,min=8"`
        ConfirmPassword string `validate:"required"`
}
// структурное правило passwordmatch зарегистрировано

form := RegistrationForm{Password: "abc", ConfirmPassword: "xyz"}
err := validate.Struct(&form)
// сколько записей и с какими тегами?

eqfield, gtfield и границы StructLevel#

Честности ради: пара полей закрывается встроенными тегами, и структурный уровень нужен не всегда. eqfield=Password проверит равенство подтверждения, gtfield=StartDate — что дата конца позже начала, required_without_all из урока про условные теги — «хотя бы один из трёх». Когда правило — сравнение двух полей без вычислений, берите тег: он короче, дешевле и уже покрыт каталогом сообщений. StructLevel берите, когда правило не выражается тегом: агрегаты по срезам, три и более поля, арифметика с датами, сравнение вычисленных значений, и когда ошибке нужно собственное имя тега с собственным сообщением вместо «failed on the eqfield tag». Вторая граница — природа проверки: структурная функция, как и теговая, не ходит в сеть и не проверяет живое состояние; «промокод существует» остаётся бизнес-слою, о чём договорились ещё в десятом уроке. И третья — не сваливайте туда то, что можно было оставить у поля: если правило трогает одно поле, ему место в теге, а структурная функция — для инвариантов целого.

Проверка: аргументы ReportError

Вы вызвали sl.ReportError(v, "Total", "Total", "summatch", ""). Что вернёт e.Tag() у этой записи?

Сообщения, тесты и порядок регистрации#

Запись из ReportError — полноценный FieldError: Field() вернёт ваше fieldName, Namespace() соберёт путь вида Order.Total с учётом вложенности, Value() — переданное значение, ActualTag() — тот же ваш тег, алиасов здесь нет. Каталог сообщений из урока про русские тексты обслуживает структурные теги на общих основаниях: ключ — Tag(), шаблон — ваш текст, Param() подставляет детали. Тесты структурных правил строятся как обычные тесты валидации: собираете структуру с расхождением, зовёте validate.Struct, ждёте ValidationErrors с вашим тегом; пятёрка значений из урока про теги превращается здесь в пятёрку комбинаций — совпало, не совпало, пустые поля, пограничная сумма, сумма из одного элемента. Регистрации держите на старте и в одном месте: RegisterStructValidation подчиняется тем же ограничениям потокобезопасности, что и RegisterValidation, — после первых проверок регистрировать новое правило значит гоняться за гонкой данных. Тот же контракт замены действует и здесь: повторная регистрация функции на том же типе молча заменит предыдущую, как и у теговых функций. Есть и нюанс кэша: структурная функция попадает в разбор типа в момент первого validate.Struct — план структуры сохраняет ссылку на неё, поэтому правило, зарегистрированное после того, как тип уже проверялся, не увидят даже новые вызовы. Ещё одна причина держать все регистрации до первой проверки. Если проект живёт в Gin или Echo, структурные правила переезжают без изменений: там тот же экземпляр движка, и RegisterStructValidation вызывается один раз при инициализации — как именно, покажут следующие два урока.

Правила структурного уровня одним списком#

  • RegisterStructValidation(fn, MyType{}) — регистрация на экземпляре, до первых проверок, тип можно без указателя
  • функция вызывается после проверки всех полей; ошибки дополняют тот же ValidationErrors
  • sl.Current() — структура целиком; Interface().(T) или FieldByName() для чтения полей
  • ReportError(field, fieldName, structFieldName, tag, param) ничего не проверяет — только пишет ошибку
  • tag — ваше имя правила; оно попадает в Tag() и в каталог сообщений
  • для пар полей без вычислений берите eqfield/gtfield; структурный уровень — агрегаты и арифметика
  • не ходите в сеть из функции: живые проверки — бизнес-слой, как и у теговых функций
  • одна функция — один тип; несколько типов через вариадик — только с type switch внутри

Соберите регистрацию структурного правила

Четыре пропуска: конструктор, тип функции, чтение структуры и рапорт об ошибке.

validate := validator.() validate.RegisterStructValidation(func(sl validator.) { form := sl.().Interface().(RegistrationForm) if form.Password != form.Confirm { sl.(form.Confirm, "Confirm", "Confirm", "passwordmatch", "Password") } }, RegistrationForm{})

Соберите путь структурного правила

Форма регистрации уходит на сервер. Разложите путь от старта до ответа.

Строки

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

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

Карточки: структурный уровень

Термин

RegisterStructValidation

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

Регистрация функции на типе: RegisterStructValidation(fn, User{}). Не потокобезопасна — только на старте; указатель не обязателен, разыменуется сам.

1 / 6

Следующий урок — валидация в Gin: ShouldBindJSON запускает этот же движок по тегам binding, и структурные правила из сегодняшнего урока переезжают туда без единой правки. Условные теги, с которых мы начали разговор о «хотя бы одном», разобраны в девятом уроке, каталог русских сообщений под структурные теги — в шестом, а табличный каркас для тестов правил — в девятнадцатом. Код урока соберите локально: песочница сайта исполняет только чистый Go и не подключает внешние модули, поэтому validator живёт в вашем терминале, а интерактивы выше проверяют контракт механики без запуска.

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

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

Вывод

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

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

  • RegisterStructValidation(fn, MyType{}) вешает функцию на тип: регистрация на старте, тип без указателя, вариадик принимает несколько типов.
  • Структурная функция вызывается после проверки полей; её ошибки дополняют тот же ValidationErrors в порядке вызовов ReportError.
  • sl.Current() — структура целиком; Interface().(T) для типизированного кода, FieldByName — по имени, Validator() вернёт сам экземпляр.
  • ReportError(field, fieldName, structFieldName, tag, param) ничего не проверяет — пишет FieldError с вашим тегом, который ключует каталог сообщений.
  • Пары полей без вычислений закрываются eqfield и gtfield; структурный уровень — для агрегатов, арифметики дат и трёх и более полей.
  • Одна функция на несколько типов — только с type switch: Interface().(T) по чужому типу паникует.
  • Структурные правила переезжают в Gin и Echo без изменений — там тот же экземпляр движка v10.

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

Возьмите Order с полями Items []Item, Total int64 и Starts/Ends time.Time. Напишите две структурные функции: summatch для суммы позиций и minduration для трёхдневного минимума. Зарегистрируйте обе в init, заведите ветки в каталоге сообщений и табличный тест: совпавшая сумма, расхождение на единицу, пустой срез позиций, событие ровно три дня, событие два дня. Убедитесь, что при расхождении полей и структуры в ValidationErrors обе записи. Добавьте и случай с пустым ConfirmPassword: требование подтверждения — тоже часть контракта, решите, ловит его required или структурная функция.

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

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

StructLevel вызовется, если поля уже провалили теги?

Да: обход полей и структурная функция независимы, вызов происходит после полей в любом случае. Ошибки окажутся в одном ValidationErrors: полевые записи первыми, структурные за ними. Именно поэтому клиенту можно отдавать полный список проблем за один вызов validate.Struct.

Почему бы не проверить пароль и подтверждение через eqfield?

Можно — eqfield=Password сделает ту же проверку. Разница в имени тега и сообщении: eqfield даст generic-запись «failed on the eqfield tag», а структурное правило — passwordmatch с собственной веткой каталога. Плюс в структурной функции правило живёт в одном месте, а не приклеено к полю подтверждения. И ещё разница — цена пересмотра: когда завтра подтверждение станет необязательным или появится третий канал пароля, тег на поле придётся переписывать у каждого потребителя структуры, а структурная функция меняется в одном месте вместе со своим тестом.

Можно ли изменить поля структуры внутри структурной функции?

Технически reflect позволяет, но не надо: функция валидации обязана быть чистым предикатом, мутация ломает порядок правил и делает Value() в ошибках устаревшим. Нормализацию и коррекцию данных делайте до validate.Struct — этому посвящён пятнадцатый урок курса.

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

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

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

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

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

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

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