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

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

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

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

Свой валидатор-функция

RegisterValidation превращает вашу функцию в полноценный тег: кастомный валидатор go выглядит в структуре как родной — validate:"phone" — и проверяет то, для чего встроенных тегов нет: телефонные маски, артикулы, чёрные списки логинов. В этом уроке пишем свои функции, читаем Field и Param, разбираемся с nil-полями и третьим аргументом и узнаём, когда достаточно RegisterAlias без кода.

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

Четвёртый урок оставил обещание: форматов в валидаторе много, но бизнес-форматы бесконечны, и телефон с кодом страны туда не влез. Часть таких случаев закрывает e164, часть — не закрывает ничем: артикул по внутреннему стандарту, логин из чёрного списка, сумма, кратная номиналу. Для них у валидатора есть RegisterValidation — функция регистрируется на экземпляре и становится тегом с собственным именем. Внутри функции — обычный Go: регулярки, словари, любые проверки без сетевого IO; снаружи — обычный тег, неотличимый от required и email. Это последняя из базовых механик курса: после неё теги перестают быть чужой грамматикой и становятся вашим API.

RegisterValidation: своя функция становится тегом#

Регистрация — метод на экземпляре валидатора: RegisterValidation("phone", fn), где fn — функция типа validator.FieldLevel, возвращающая bool. С этого момента тег phone работает в любой структуре этого экземпляра как встроенный: встанет в цепочку правил, подчинится omitempty и условным тегам, попадёт в FieldError с собственным именем. Регистрировать нужно на старте, до первых проверок: реестр правил не потокобезопасен во время валидации — та же дисциплина второго урока, где один экземпляр живёт на всё приложение. Место регистрации тоже выбирайте один раз: пакет, где создаётся экземпляр, обычно рядом с конструктором — так ревьюер видит весь словарь тегов проекта на одном экране, а тесты собирают тот же набор вызовов без копипасты. Реестр ведёт себя иначе, чем подсказывает интуиция. Повторная регистрация того же имени не падает: документация прямо говорит — если тег уже существует, предыдущая функция заменяется новой. Тихая замена опаснее паники: два пакета, зарегистрировавшие один тег по-разному, дают валидатор, который работает по правилам того, кто инициализировался последним, — поэтому держите все RegisterValidation в одном месте, где конфликт виден на ревью. Жёстко защищены служебные теги: попытка зарегистрировать required, omitempty, dive, keys, endkeys или structonly падает паникой «Tag 'required' either contains restricted characters or is the same as a restricted tag needed for normal operation» — они грамматика разбора тега, и валидатор их не отдаёт. Остальные встроенные — min, max, email — формально перезаписываются без ошибки: не приглашение, а дыра, в которую легко наступить; своё поведение называйте своим именем. Имя тега выбирайте как имя функции: латиница, без пробелов и запятых — грамматика тега не оставляет места для вольностей. И держите функцию чистой: предикат, который берёт значение и отвечает bool, без изменения состояния, без времени и сети — причины разобраны ниже, а пока договорённость.

Тег phone: регулярка за десяток строк

GO
var phoneRe = regexp.MustCompile("^[+][0-9]{7,15}$")

validate := validator.New()
validate.RegisterValidation("phone", func(fl validator.FieldLevel) bool {
        return phoneRe.MatchString(fl.Field().String())
})

type ContactForm struct {
        Phone string `validate:"required,phone"`
}

// "+79161234567" → проходит: плюс и 11 цифр
// "89161234567"  → провал: тег phone, Param пуст

Регулярка скомпилирована один раз на уровне пакета — компиляция в самой функции означала бы пересборку выражения на каждое поле. fl.Field() возвращает reflect.Value поля, String() читает строку. required стоит первым: nil и пустоту он ловит до функции, поэтому внутри можно не думать о нуле. Провал попадает в FieldError с Tag() равным phone — имя своё, механика чужая.

Проверка: двойная регистрация

На одном экземпляре валидатора RegisterValidation("phone", fn) вызвали второй раз с другой функцией. Что произойдёт?

FieldLevel: что можно читать в функции#

МетодЧто отдаёт
Field()reflect.Value поля: String(), Int(), Kind(), IsNil()
Param()параметр тега строкой — всё после знака равенства
FieldName()имя поля; после RegisterTagNameFunc — json-имя
StructFieldName()имя поля из объявления структуры — даже после RegisterTagNameFunc
Parent()родительская структура — соседние поля одним движением
Top()корневая структура вызова Struct()
GetTag()имя тега, по которому вызвали функцию

Параметр тега: fl.Param() и гибкие правила#

Тег может нести параметр после знака равенства — это fl.Param() внутри функции, строка без трюков: «5» для multiple=5, перечень через пробел для списка значений. Параметр — способ сделать один тег семейством правил вместо десяти тегов на каждый случай: multiple=5 и multiple=100 обслуживаются одной функцией, а если завтра бизнес попросит кратность двадцати, тег в структуре изменится без правки кода проверки — в этом весь смысл параметризации. Механика чтения честная: Param() приходит строкой, и разбор — ваша работа — strconv.Atoi с честной обработкой ошибки. Ошибка параметра — это ошибка программиста, не данных: тег написали руками, и «5о» в нём обнаружится на первом же тесте; функция возвращает false, и провал всплывает с именем тега. Внутри функции стоит дисциплина отражения: прежде чем звать Int() или String(), проверьте Kind() — тег, повешенный не на то поле, обязан честно провалиться, а не поднять панику reflect. Для значений-перечней держите грамматику из третьего урока: пробел внутри параметра разделяет значения, запятая между правилами — конвейер. И не забывайте Parent() из таблицы: функция видит родительскую структуру, поэтому «сумма кратна номиналу, но не больше остатка» может читаться из соседнего поля — а полноценные кросс-польные контракты это мост к StructLevel из одиннадцатого урока.

Два тега: замыкание и параметр

GO
// Замыкание: чёрный список загружен на старте из конфига.
var reserved = map[string]bool{"admin": true, "root": true}

validate.RegisterValidation("notreserved", func(fl validator.FieldLevel) bool {
        return !reserved[fl.Field().String()]
})

// Параметр после знака равенства приходит в fl.Param().
validate.RegisterValidation("multiple", func(fl validator.FieldLevel) bool {
        if fl.Field().Kind() != reflect.Int {
                return false // тег повесили не на то поле
        }
        n, err := strconv.Atoi(fl.Param())
        if err != nil || n == 0 {
                return false // битый параметр — честный провал
        }
        return fl.Field().Int()%int64(n) == 0
})

// Username string `validate:"required,notreserved,min=3"`
// Amount    int    `validate:"required,multiple=5"`

notreserved читает замыкание: словарь загружен на старте, функция остаётся чистым предикатом без IO. multiple разбирает Param() через strconv и дисциплину Kind: не-число и не-целое поле проваливаются явно, а не паникуют. Оба тега используются как встроенные — цепочка правил и omitempty работают без оговорок.

nil-поля и третий аргумент#

У собственных функций есть тихое соглашение, которое проявляется только на указателях и интерфейсах: по умолчанию валидатор НЕ вызывает вашу функцию на nil-поле. Указатель пуст — правило пропускается, поле считается пройденным, и это выглядит логично: проверять нечего. Но молчаливый пропуск — источник недоумений: тег есть, а проверки как будто нет. Если функция обязана видеть даже nil — например, чтобы различать «не указано» и «указано пусто» — у RegisterValidation есть третий вариадический аргумент: RegisterValidation("phone", fn, true) вызывает функцию и на нулевых полях, и внутри вы сами решаете, что с ними делать, через fl.Field().IsNil(). Комбинации с остальными тегами при этом обычные: required перед вашим тегом отловит nil и не пустит выполнение дальше, omitempty пропустит пустоту по договору опциональности. Практический вывод: свой тег на указателе — всегда случай из трёх, и тест должен показать, какой из них ваш: nil пропущен, nil проваливается, nil передаётся в функцию. Третий аргумент выручает там, где nil — легитимное значение с собственным смыслом: «не указано» в полях отчётности, дата без времени, опциональная ссылка на документ. Без теста разница живёт только в голове автора, а после рефакторинга живёт там неправильно.

Что вернёт проверка профиля

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

type Profile struct {
        Nickname string  `validate:"omitempty,notreserved"`
        Backup   *string `validate:"notreserved"`
}

p := Profile{Nickname: "", Backup: nil}
err := validate.Struct(&p)
// что в err?

RegisterAlias: композиция вместо кода#

Прежде чем писать функцию, спросите: а не собирается ли мой тег из существующих правил? RegisterAlias регистрирует новое имя для готовой комбинации: RegisterAlias("phone", "e164") — и тег phone означает встроенный формат E.164, без единой строки логики. Значение алиаса — готовая строка правил: с параметрами и с разделителем | для варианта «любое из», как в классическом примере с цветовыми форматами. Алиас не проверяет ничего сам — он словарь имён, и в этом его сила: правило меняется в одном месте, а теги по проекту остаются. С алиасами связана тонкость из пятого урока: Tag() вернёт ваше имя — phone, а ActualTag() — реальный тег e164; каталог сообщений ключуйте по Tag(), чтобы сообщение «телефон в формате +7...» жило под именем, которое видит ревьюер. Регистрация алиасов — на том же старте и с той же не-потокобезопасностью, что у RegisterValidation. Правило выбора простое: комбинация существующих правил — алиас; новая логика, которой нет среди встроенных тегов — функция. Если валидатору можно объяснить телефон через e164 — не пишите регулярку, форматы из четвёртого урока уже проверены сообществом.

Проверка: параметр в FieldError

Amount int с тегом multiple=5 провалил проверку. Что вернёт e.Param()?

Сообщения для своих тегов и тесты правил#

Свой тег попадает в FieldError со своим именем — и каталог из шестого урока должен его знать: ветка case "phone" в tagTexts с формулировкой «телефон в формате +7XXXXXXXXXX». Без ветки сообщение уедет в default с технической строкой — тот же признак дыры в каталоге, что и у встроенных тегов. Тесты правил — вторая половина дисциплины: тег — строка в структуре, компилятор его не проверяет, а ваша функция проверяет только то, что ей дали. Пятёрка значений из третьего урока здесь работает в полный рост: валидное значение, валидное на границе, невалидный формат, пустое, nil для указателей — и к ним стоит добавить случай «тег повешен не на тот тип», если функция проверяет Kind. Табличные тесты для правил — готовый каркас из девятнадцатого урока, а регистрация в тестах использует тот же экземпляр, что и приложение. Имена тегов и ветки каталога стоит проверять одной связкой: тест, который прогоняет все зарегистрированные теги через пустую форму, заодно проверяет и полноту каталога — две защиты одной строкой. Отдельный тест на Param() не повредит: битый параметр сегодня — это false на всех полях сразу, и лучше узнать об этом в CI, чем в тикете «всё сломалось с обновлением».

Правила своих валидаторов одним списком#

  • функция — чистый предикат bool: без IO, без времени, без состояния запроса
  • регистрация — на старте, до первых проверок, в одном месте пакета
  • повторная регистрация молча заменяет функцию; служебные теги вроде required и dive — паника
  • fl.Field() — reflect.Value: проверяйте Kind(), прежде чем звать String() и Int()
  • Param() — одна строка после знака равенства; перечни — через пробел внутри
  • по умолчанию nil-поле функцию не вызывает; третий аргумент true меняет контракт
  • комбинация существующих правил — RegisterAlias, новая логика — функция
  • каждому тегу — ветка в каталоге сообщений и тест с пятёркой значений

Соберите регистрацию тега

Четыре пропуска: конструктор, регистрация, вызов регулярки и чтение поля.

validate := validator.() validate.("phone", func(fl validator.FieldLevel) bool { return phoneRe.(fl.Field().()) })

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

Логин «admin» приходит на регистрацию. Разложите путь от старта приложения до ответа.

Строки

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

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

Карточки: свои теги

Термин

RegisterValidation

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

Метод экземпляра: RegisterValidation("phone", fn) делает функцию тегом. Регистрация на старте; повторная — тихая замена функции, а служебные теги вроде required защищены паникой.

1 / 6

Одиннадцатый урок — StructLevel: правила на всю структуру: когда «сумма позиций равна итогу» не лезет ни в один тег поля, начинается валидация целого объекта. Форматы, на которых строились алиасы, разобраны в четвёртом уроке, каталог сообщений — в шестом. Код тега phone и чёрного списка соберите локально: песочница сайта исполняет только чистый Go без внешних модулей, поэтому свои валидаторы живут в вашем терминале, а интерактивы проверяют контракт функций без запуска.

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

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

Вывод

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

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

  • RegisterValidation превращает функцию в тег: имя своё, механика встроенная; повторная регистрация молча заменяет функцию — держите теги в одном месте.
  • FieldLevel даёт Field(), Param(), Parent() и Top(): значение, параметр, соседние поля и корень структуры.
  • Параметр тега — строка в Param(): разбор strconv, дисциплина Kind, битый параметр — честный провал.
  • Функция — чистый предикат: IO, время и состояние запроса остаются бизнес-слою; данные для проверки — в замыкании на старте.
  • По умолчанию функция на nil-поле не вызывается; третий аргумент true заставляет её видеть и nil.
  • Комбинация существующих правил — RegisterAlias без кода; новая логика — функция.
  • Свой тег обязан иметь ветку в каталоге сообщений и табличный тест с пятёркой значений.

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

Реализуйте тег inn: функция проверяет десять цифр и контрольное число по алгоритму ИНН физлица. Зарегистрируйте на экземпляре из второго урока, добавьте ветку в каталог сообщений и табличный тест: валидный ИНН, валидный на границе контрольного числа, невалидная контрольная сумма, буквы в строке, пустая строка, nil на *string. Потом напишите RegisterAlias("phone", "e164") и объясните себе, когда ваш телефонный тег из урока честнее алиаса.

Личные заметкиПусто
Хранятся только в вашем браузере (Validator · Свой валидатор-функция)

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

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

Служебные теги — required, omitempty, dive, keys, endkeys, structonly — нет: RegisterValidation падает паникой «Tag ... either contains restricted characters ...». Остальные встроенные, вроде min и email, перезапишутся молча, но делать этого не стоит: код, читающий FieldError по Tag(), разъедется с фактическими правилами. Регистрируйте новое имя, а для «телефон — это e164» проще RegisterAlias с новым именем: он не пишет логики вовсе.

Почему функция не вызывается на пустой строке?

Соглашение касается nil: на nil-указателях и nil-интерфейсах функция не вызывается, а пустая строка — обычное значение, и функция её получает. Если хотите пропускать пустые строки до своей проверки, ставьте omitempty перед тегом — это обычный конвейер правил из третьего урока.

Где жить правилу «логин уже занят»?

Не в валидаторе. Это вопрос живого состояния, а не формата: он требует запроса к базе, своих ретраев и своего контракта ошибок — обычно 409 вместо 400. Валидатор проверяет форму значения, бизнес-слой — занятость; смешение даёт таймауты базы внутри рефлексивного цикла и 400 на сетевые сбои.

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

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

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

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

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

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

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