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

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

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

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

Условные теги: required_without, excluded_with

Условная валидация go начинается там, где required на каждом поле ломает форму: клиент указывает email или телефон, но не оба. required_without и его семья описывают зависимости полей прямо в тегах — обязательность по соседям, запреты на совместность, обязательность по значению другого поля — и в этом уроке мы разберём всю семью вместе с ловушкой порядка omitempty.

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

Форма контактов из прошлых уроков прячет тупик. Поставим 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 с двумя остальными соседами в роли условия — за счёт перечней семья закрывает формы, которые в ручном коде превращаются в матрицу состояний.

Форма контактов: хотя бы один способ связи

GO
type 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().

Форма доставки: зависимость от способа

GO
type 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?

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() — перечень соседей или «Поле значение»
  • цепочка из трёх и больше зависимостей — сигнал переезжать на отдельные структуры или свои функции

ошибки условных тегов: что читать в 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 дадут две записи; не склеивайте их в одну на бэкенде, фронтенду проще подсветить оба поля.

Соберите условия формы

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

type DeliveryForm struct { Method string `validate:"required,oneof=courier pickup"` Address *Address `validate:"=Method courier,=Method pickup"` } type ContactForm struct { Email string `validate:"required=Phone,omitempty,email"` Phone string `validate:"required=Email,omitempty"` }

Соберите путь условной ошибки

Клиент выбрал курьера и не указал адрес. Разложите события по порядку.

Строки

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

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

Карточки: семья условий

Термин

required_without

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

Поле обязательно, если хотя бы один из перечисленных соседей отсутствует. Соседи — через пробел в параметре: required_without=Phone Email.

1 / 6

Десятый урок — свой валидатор-функция: 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 в форме контактов и найдите состояние, при котором пустая форма проходит валидацию, — вы только что увидели цену перестановки в теге.

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

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

Почему обе записи 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.

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

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

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

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

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

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

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