---
title: "Условные теги: required_without, excluded_with"
description: "required_without и условная валидация go: семья required_with и excluded, required_if, skip_unless — зависимости полей и ловушка omitempty с тестами."
course: "Валидация в Go: go-playground/validator"
url: "https://golangmaster.ru/courses/validator/lessons/09-uslovnye-tegi/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

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

*Курс «Валидация в Go: go-playground/validator», урок 9 из 25. ~20 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/validator/lessons/09-uslovnye-tegi/*

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

Форма контактов из прошлых уроков прячет тупик. Поставим required и на Email, и на Phone — и получим форму, в которой нельзя оставить только почту: телефон не нужен, но поле его требует. Снимем required и оставим omitempty — получим дыру: пустая форма контактов проходит валидацию целиком, и заказ уезжает без единого способа связи. Между «всё обязательно» и «всё опционально» живёт третья формулировка — «хотя бы одно из», «только если заполнено другое», «не вместе с этим». Для неё у валидатора есть семья условных тегов: required_without, required_with, excluded_with и компания плюс required_if для зависимостей от значения. Семантика знакомая — та же не-нулевость из третьего урока, только условие смотрит на соседние поля, а не на само поле.

### условная валидация go: обязательность по соседям

Семья required_with и required_without отвечает на вопрос «когда поле обязательно», и условие читается по соседям, перечисленным через пробел после знака равенства. required_without=Phone означает: поле обязательно, если Phone отсутствует — а «отсутствует» здесь то же самое, что в [семантике required из третьего урока](/courses/validator/lessons/03-tegi-required-min-max/): для строк, чисел и 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?

- Ничем: _all — просто алиас для совместимости
- ✅ _all: поле обязательно, только когда ВСЕ перечисленные поля отсутствуют; без _all — когда отсутствует хотя бы один
- _all: правило применяется ко всем полям структуры сразу
- _all: правило игнорирует omitempty и работает даже на пустом поле

Суффикс _all переводит условие на «все перечисленные»: required_without_all=Phone Email делает поле обязательным только при полном отсутствии обоих соседей. Без _all достаточно отсутствия любого одного. Ошибка в выборе суффикса меняет договор всей формы — прогоняйте четыре состояния, прежде чем коммитить.

### Семья условных тегов одной таблицей

| Тег | Поле обязано быть... |
| --- | --- |
| 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» — разные значения, поэтому нормализуйте способ доставки до валидации, иначе условие молча перестанет ловить половину вариантов. Когда же условие выходит за рамки равенства — «адрес обязателен для доставки за пределы города, но не в день праздника» — теги заканчиваются, и начинаются [свои функции-валидаторы десятого урока](/courses/validator/lessons/10-kastomnye-funkcii/), которым условия доступнее: они видят всю структуру через 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 означает «объекта нет», и условие честно различает отсутствие и пустую структуру.

### Что вернёт проверка контактов

```go
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?
```

**Что выведет программа?**

- ✅ nil: Email не обязателен при заполненном телефоне, Phone присутствует
- Ошибка email: пустая строка не адрес почты
- Ошибка required_without по Email: поле пустое
- Ошибка required_without по Phone: телефон заполнен без почты

У Email: сосед Phone присутствует, условие required_without ложно — поле не обязательно; omitempty пропускает формат на пустой строке. У Phone: сосед Email отсутствует, поле обязательно — и оно заполнено. Оба поля чисты, err равен nil. Уберите Phone — появится ошибка required_without на Email, а не на Phone.

> **Живое место: omitempty раньше условного required.** Перестановка в теге — 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 пустой. Что вернёт валидатор?

- Ошибка min: пустая строка короче четырёх символов
- ✅ nil: omitempty пропустил все правила пустого поля
- Ошибка required_if: HasPromo требует промокод
- Ошибка oneof: значение Promo не из перечня

omitempty стоит первым: поле нулевое, и остальные правила тега пропускаются — включая required_if и min. Условная обязательность под omitempty не срабатывает никогда, потому что до неё дело не доходит. Правильный порядок — required_if=HasPromo 1 первым: тогда пустой Promo при HasPromo=1 провалит условие.

### 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-границы](/courses/validator/lessons/16-dto-granicy/).

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

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

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

```go
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_if; запрещён при значении — excluded_if; обязателен без соседа — required_without.

Правильные ответы в пропусках: `required_if`, `excluded_if`, `without`, `without`

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

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

Правильный порядок строк:

1. `Клиент отправляет DeliveryForm: Method courier, Address null`
2. `Хендлер декодирует JSON: Address остался nil-указателем`
3. `Method проходит required и oneof`
4. `required_if=Method courier видит: Method равен courier`
5. `Address nil — объект отсутствует`
6. `Поле проваливает required_if`
7. `FieldError несёт Tag() required_if и Param() «Method courier»`
8. `Каталог собирает «укажите адрес доставки», ответ 400 уходит клиенту`

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

- **required_without** — Поле обязательно, если хотя бы один из перечисленных соседей отсутствует. Соседи — через пробел в параметре: required_without=Phone Email.
- **required_without_all** — Обязателен, только когда ВСЕ перечисленные соседи отсутствуют. Суффикс _all переводит условие с «хотя бы один» на «все» — главная точка путаницы семьи.
- **excluded_with** — Поле должно быть пустым, если хотя бы один сосед заполнен: взаимоисключающие промокод и бонусы. «Пусто» = нулевое значение типа.
- **required_if / excluded_if** — Зависимость от значения: required_if=Method courier — обязательно при courier, excluded_if — пусто при условии. Параметр — пары «поле значение»; несколько условий — повторением имени поля, нечётное число параметров — паника.
- **skip_unless** — Условная обязательность по парам: совпали ВСЕ пары «поле значение» — поле обязано быть непустым, не совпали — проходит только сам тег. От required_if отличается семантикой «все условия сразу» вместо «любое».
- **порядок с omitempty** — Условный required — первым, omitempty — вторым: на нулевом поле omitempty пропускает все правила после себя, поэтому условие, поставленное после него, не сработает никогда.

Десятый урок — [свой валидатор-функция](/courses/validator/lessons/10-kastomnye-funkcii/): RegisterValidation, тег phone и функции, которым условия доступны целиком. Теги, которые разрослись в язык внутри строки, — один из пунктов [урока про антипаттерны](/courses/validator/lessons/21-antipatterny/); загляните туда, когда захочется проверить свою форму на симптомы. Код формы доставки соберите локально: песочница сайта исполняет только чистый Go без внешних модулей, поэтому условные теги живут в вашем терминале, а интерактивы проверяют состояния формы без запуска.

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

- Условные теги описывают «когда поле обязательно» и «когда поле запрещено» по соседям и по значениям.
- 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.
