Содержание урока · 10 разделов
- условная валидация go: обязательность по соседям
- Семья условных тегов одной таблицей
- required_if и excluded_if: зависимость от значения
- required_without и omitempty: правильный порядок
- excluded_with: взаимоисключающие поля
- Чек-лист условной валидации одним списком
- ошибки условных тегов: что читать в FieldError
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Форма контактов из прошлых уроков прячет тупик. Поставим required и на Email, и на Phone — и получим форму, в которой нельзя оставить только почту: телефон не нужен, но поле его требует. Снимем required и оставим omitempty — получим дыру: пустая форма контактов проходит валидацию целиком, и заказ уезжает без единого способа связи. Между «всё обязательно» и «всё опционально» живёт третья формулировка — «хотя бы одно из», «только если заполнено другое», «не вместе с этим». Для неё у валидатора есть семья условных тегов: required_without, required_with, excluded_with и компания плюс required_if для зависимостей от значения. Семантика знакомая — та же не-нулевость из третьего урока, только условие смотрит на соседние поля, а не на само поле.
условная валидация go: обязательность по соседям#
Семья required_with и required_without отвечает на вопрос «когда поле обязательно», и условие читается по соседям, перечисленным через пробел после знака равенства. required_without=Phone означает: поле обязательно, если Phone отсутствует — а «отсутствует» здесь то же самое, что в семантике required из третьего урока: для строк, чисел и bool — нулевое значение («», 0, false), для указателей, срезов, карт и интерфейсов — nil. Нюанс, который дорого стоит на формах: пустой, но инициализированный срез для условия — присутствующий сосед, отсутствует только nil-срез; после декодирования JSON именно nil означает «поля в запросе не было». Условная валидация go устроена как словарная статья: поле описывает не «когда я валидно», а «когда я обязана существовать». Вариант с _all меняет логику на «все перечисленные»: required_without_all=Phone Email — поле обязательно, только если нет ни телефона, ни почты, то есть всегда, кроме случаев, когда хоть один способ связи указан. Разница между «хотя бы одного нет» и «все отсутствуют» — главная путаница семьи, и её стоит прогнать на четырёх состояниях формы до того, как тег поедет в прод. Присутствие соседа, напомним, тоже нулевое-значение-перевёрнутое: строка из пробелов — присутствующая, ноль числа — отсутствующий, и это ровно тот случай, где указатель из третьего урока снова становится инструментом честности. Соседей в условии можно перечислять сколько нужно через пробел, но помните о суффиксе: required_without=Phone Email Telegram — «обязательно, если хотя бы одного из трёх нет»; «только когда нет ни одного» — это required_without_all. Для формы «хотя бы один способ связи» из трёх каналов каждому полю хватает required_without_all с двумя остальными соседами в роли условия — за счёт перечней семья закрывает формы, которые в ручном коде превращаются в матрицу состояний.
Форма контактов: хотя бы один способ связи
GOtype ContactForm struct {
Email string `validate:"required_without=Phone,omitempty,email"`
Phone string `validate:"required_without=Email,omitempty"`
}
// оба пустые → обе записи required_without: ни одного способа связи
// Email заполнен → Phone не обязателен: omitempty пропустил пустоту
// оба заполнены → оба поля валидируются по своим форматам
// Email "плохой" → ошибка email: required_without прошёл, формат — нетДоговор читается с двух сторон зеркально: Email обязателен без телефона, Phone — без почты. Порядок правил в теге принципиален: условная обязательность стоит первой, omitempty второй — пропускает форматы на пустом поле. Заполненное поле проверяется форматом независимо от соседа — «хотя бы один» не отменяет «заполненный должен быть корректным».
Проверка: _without против _without_all
Чем required_without_all отличается от required_without?
Семья условных тегов одной таблицей#
| Тег | Поле обязано быть... |
|---|---|
| required_with | заполнено, если хотя бы один сосед заполнен |
| required_with_all | заполнено, если все соседи заполнены |
| required_without | заполнено, если хотя бы одного соседа нет |
| required_without_all | заполнено, если все соседи отсутствуют |
| excluded_with | пустым, если хотя бы один сосед заполнен |
| excluded_with_all | пустым, если все соседи заполнены |
| excluded_without | пустым, если хотя бы одного соседа нет |
| excluded_without_all | пустым, если все соседи отсутствуют |
| required_if | заполнено, если другое поле равно значению |
| excluded_if | пустым, если другое поле равно значению |
| skip_unless | обязательным, если ВСЕ пары «поле значение» совпали; иначе — свободным |
required_if и excluded_if: зависимость от значения#
Вторая ветка семьи смотрит не на соседей, а на значение конкретного поля: required_if=Method courier — поле обязательно, если Method равен courier. Параметр — пары «поле значение», разделённые пробелом: required_if=Method courier; несколько условий расширяются повторением имени поля — required_if=Method courier Method taxi делает поле обязательным при courier ИЛИ taxi. Следите за парностью: нечётное число параметров — паника «Bad param number for required_if» на первой же проверке структуры. Форма доставки собирается на этих двух тегах полностью: адрес обязателен для курьера, адрес запрещён для самовывоза, а Comment остаётся опциональным всегда. Так пара тегов описывает бизнес-правило «всё зависит от способа доставки», которое иначе пришлось бы расписывать ветками if в хендлере. Есть и обратный вариант — required_unless: поле обязательно, ПОКА условие не выполнено; он читается тяжелее и нужен реже, держите в памяти как существующий, а применяйте required_if. Пара required_if и excluded_if покрывает большинство «если-то» форм: обязательность по значению и запрет по значению. Только помните о регистре: значение сравнивается буквально, «courier» и «Courier» — разные значения, поэтому нормализуйте способ доставки до валидации, иначе условие молча перестанет ловить половину вариантов. Когда же условие выходит за рамки равенства — «адрес обязателен для доставки за пределы города, но не в день праздника» — теги заканчиваются, и начинаются свои функции-валидаторы десятого урока, которым условия доступнее: они видят всю структуру через Parent().
Форма доставки: зависимость от способа
GOtype DeliveryForm struct {
Method string `validate:"required,oneof=courier pickup"`
Address *Address `validate:"required_if=Method courier,excluded_if=Method pickup"`
Comment string `validate:"omitempty,max=200"`
}
// courier + nil-адрес → провал required_if: курьеру нужен адрес
// pickup + адрес → провал excluded_if: самовывоз без адреса
// pickup + nil → оба условия ложны, поле свободно
// пустой Method → required сверху: oneof не пропустит мусорДва условных тега на одном поле дают полный контракт: required_if требует объект для курьера, excluded_if запрещает его для самовывоза. Method проверяется обычным required с oneof — условные теги не заменяют базовую обязательность. Address — указатель из седьмого урока: nil означает «объекта нет», и условие честно различает отсутствие и пустую структуру.
Что вернёт проверка контактов
Прочитайте код и выберите, что он выведет:
type ContactForm struct {
Email string `validate:"required_without=Phone,omitempty,email"`
Phone string `validate:"required_without=Email,omitempty"`
}
c := ContactForm{Phone: "+79161234567"}
err := validate.Struct(&c)
// что в err?Перестановка в теге — omitempty,required_without=Phone — тихо выключает условную обязательность. omitempty пропускает остальные правила на нулевом поле, и required_without попадает под пропуск: «хотя бы один способ связи» превращается в «можно ничего», а тесты с заполненной формой зелёные. Обнаруживается на интеграции, когда анкета без контактов доезжает до CRM. Лечится порядком: условный required первым, omitempty вторым, форматы после — и тестом пустой формы в каждом состоянии соседа.
required_without и omitempty: правильный порядок#
Механика порядка заслуживает отдельного абзаца, потому что на ней держатся все комбинации урока. Правила тега исполняются слева направо, и omitempty работает как выключатель для того, что стоит после него: поле нулевое — оставшиеся правила пропускаются целиком. Условные теги — обычные правила в этом конвейере: они исполняются в свою очередь и либо требуют поле, либо отпускают. Отсюда канонический порядок: условная обязательность, omitempty, форматы и границы. Условие первым — оно решает судьбу пустого поля; omitempty вторым — он не даёт min, max и email добить пустоту, которую условие только что отпустило. Проверяйте комбинацию пятёркой состояний: оба поля пусты, заполнен первый, заполнен второй, заполнены оба, заполнен первый некорректно — это минимальный набор, на котором видно и условие, и формат. Отдельная память на числа: ноль — нулевое значение, то есть «отсутствует» для условия. Этаж 0 при required_without=Level будет считаться неуказанным, и поле потребуют заново; если ноль легитимен, переходите на указатель из третьего урока — *int различает «не указано» и «указан ноль» для тегов и для условий одинаково. Из механики следует и приём отладки: если сомневаетесь, что условие сработало, прогоните структуру с нулевым полем при каждом состоянии соседа и посмотрите на Tag() в ответе — условный тег, который не появился, либо не в том порядке, либо его условие читается иначе, чем вам кажется.
Проверка: порядок правил в теге
Поле Promo с тегом omitempty,required_if=HasPromo 1,min=4. HasPromo равен 1, Promo пустой. Что вернёт валидатор?
excluded_with: взаимоисключающие поля#
Обратная задача — не «должно быть заполнено», а «должно быть пусто»: excluded_with=BonusPoints запрещает заполнять поле, пока заполнен BonusPoints. Классический носитель — способы применения скидки: промокод и бонусные баллы нельзя совместить, и бизнес хочет это на границе, а не в глубине логики. Тег описывает конфликт одной строкой на каждое поле пары, а сообщение из каталога шестого урока собирается по Tag() и Param(): «выберите одно: промокод или бонусы». Пара зеркальных excluded_with закрывает конфликт с обеих сторон: какое поле ни заполнили вторым, ошибка укажет на него. Тонкость, которая возвращается из третьего урока: «пусто» — это нулевое значение, и ноль числа считается пустым. Счётчик бонусов со значением 0 под excluded_with пройдёт — поле «пустое», конфликта нет; если для бизнеса «0 бонусов» — это применённый бонус, нужна пара указателей, где nil и ноль различимы. И следите за направлением: excluded_with говорит «сосед есть — я пуст», excluded_without — «соседей нет — я пуст»; суффиксы работают как в required-семье, только вердикт противоположный, и таблица выше — лучший способ не перепутать их в боевом теге. И не пытайтесь заменить excluded_with на oneof с пустым значением: перечень допустимых значений не умеет говорить «поле должно быть пустым», а пустая строка в oneof выглядит легитимным выбором и путает клиента — для запрета существует отдельный тег, вот им и пользуйтесь.
Чек-лист условной валидации одним списком#
- условие читается по нулевым значениям: "", 0, false, nil — «отсутствует»; пустой, но не nil срез — присутствующий сосед
- порядок в теге: условный required первым, omitempty вторым, форматы и границы после
- соседи перечисляются через пробел: required_without=Phone Email; в required_if — пары «поле значение», несколько условий — повторением имени поля
- пятёрка состояний на пару полей: оба пусты, каждый по отдельности, оба заполнены, первый некорректен
- ноль числа — «отсутствует»: для честного нуля нужны указатели из третьего урока
- Tag() возвращает имя условия, Param() — перечень соседей или «Поле значение»
- цепочка из трёх и больше зависимостей — сигнал переезжать на отдельные структуры или свои функции
Я долго выбирал между required_if-цепочками и отдельными DTO под каждый вариант формы — и мой порог такой: две-три ветки с зависимостями на одно-два поля живут в тегах, дальше — отдельные структуры, потому что каждая ветка становится типом с собственными тестами, а не условием, спрятанным в строке тега. Условие правоты тегов: форма одна, меняется состав полей, а не смысл; как только появляется новый способ доставки с новым набором полей — теги разрастаются в язык внутри строки, и это плохой язык. Разделение слоёв, где это решение живёт, — тема урока про DTO-границы.
ошибки условных тегов: что читать в FieldError#
FieldError на условном теге несёт всё то же сырьё из пятого урока, и каталогу сообщений достаточно Tag() и Param(). Tag() возвращает имя условия — required_without, excluded_if; по нему каталог отличает «обязательное поле» от «обязательное, если не указан телефон». Param() возвращает перечень соседей строкой: «Phone» для одного, «Phone Email» для двух, а для required_if — «Method courier», имя поля и значение в одном параметре. Шаблоны сообщений получаются честными: «укажите хотя бы один способ связи», «поле обязательно при Method courier», «выберите что-то одно». Namespace() здесь работает как всегда — на вложенных формах путь покажет, какая ветка дерева сломалась. Диагностика из Value() в логах дополняет картину: запись с полем, соседом и значением соседа позволяет восстановить состояние запроса без трейсинга — ровно то, что нужно дежурному в три ночи. Две вещи, которых Tag() и Param() не дадут. Первая: почему сосед посчитался отсутствующим — нулевое значение скрыто внутри условия, и для диагностики берите Value() в логах, не в ответе. Вторая: список всех конфликтующих пар разом — валидатор собирает ошибки по полям независимо, и два зеркальных excluded_with дадут две записи; не склеивайте их в одну на бэкенде, фронтенду проще подсветить оба поля.
Соберите условия формы
Четыре пропуска: обязательность по значению, запрет по значению и зеркальная пара контактов.
Соберите путь условной ошибки
Клиент выбрал курьера и не указал адрес. Разложите события по порядку.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: семья условий
Десятый урок — свой валидатор-функция: RegisterValidation, тег phone и функции, которым условия доступны целиком. Теги, которые разрослись в язык внутри строки, — один из пунктов урока про антипаттерны; загляните туда, когда захочется проверить свою форму на симптомы. Код формы доставки соберите локально: песочница сайта исполняет только чистый Go без внешних модулей, поэтому условные теги живут в вашем терминале, а интерактивы проверяют состояния формы без запуска.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Условные теги описывают «когда поле обязательно» и «когда поле запрещено» по соседям и по значениям.
- required_without — обязателен, если хотя бы одного соседа нет; _all переводит условие на «все отсутствуют».
- excluded_with запрещает совместность: сосед заполнен — поле должно быть пустым; ноль числа считается пустым.
- required_if и excluded_if смотрят на значение: параметр — «Поле значение», несколько значений через пробел.
- Порядок в теге: условный required первым, omitempty вторым — иначе omitempty глушит условие.
- Пятёрка состояний пары полей — минимальный тест условной валидации; ноль числа — «отсутствует», для честного нуля указатель.
- Tag() и Param() дают материал сообщений: имя условия и перечень соседей или «Поле значение».
Домашнее задание
Соберите форму доставки с Method, Address и Comment и добавьте пару промокод-бонусы с зеркальными excluded_with. Прогоните локально пять состояний: оба контакта пусты, только email, только телефон, оба заполнены, email заполнен некорректно. Запишите Tag() и Param() каждой ошибки. Потом поменяйте порядок на omitempty,required_without в форме контактов и найдите состояние, при котором пустая форма проходит валидацию, — вы только что увидели цену перестановки в теге.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему обе записи required_without появились, когда оба контакта пусты?
Поля проверяются независимо: Email видит отсутствующий Phone и требует заполнения, Phone видит отсутствующий Email — и требует тоже. Клиент получает обе записи и подсвечивает оба поля, что честно: действительно ни один способ связи не указан. Если хочется одно сообщение на форму, собирайте его на фронте или в StructLevel-правиле одиннадцатого урока.
Можно ли в required_if сравнивать поле с несколькими значениями?
Да, но парами: required_if=Method courier Method taxi — поле обязательно при courier или taxi. Условия расширяются повторением имени поля, а не перечнем значений: параметр разбирается парами «поле значение», и нечётное число параметров падает паникой «Bad param number for required_if». Сравнение строгое, по строке; для чисел учитывайте, что значение параметра — строка, и сверяйте формат с тем, что реально приходит в поле.
Чем skip_unless отличается от omitempty?
Они про разное. omitempty смотрит на само поле и на нулевом значении глушит все правила после себя. skip_unless — условная обязательность по парам «поле значение»: совпали все — поле должно быть непустым, не совпали — проходит только сам этот тег, а соседние правила вроде min выполняются как обычно. Комбинация «проверяй только при включённом флаге» собирается порядком из урока: skip_unless первым, omitempty вторым — пустое поле при выключенном флаге отпускается, при включённом — проваливает skip_unless.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Asynq в Go: очередь фоновых задач на Redis
Курс asynq для Go: клиенты и воркеры, отложенные и периодические задачи, ретраи, очереди с приоритетами, агрегация, Inspector и asynqmon. 25 уроков.
- Строить клиент и воркер asynq и подключать Redis
- Отправлять отложенные, периодические и уникальные задачи
- Настраивать ретраи, таймауты и приоритеты очередей
- Мониторить задачи через Inspector и asynqmon