---
title: "Срезы и карты: тег dive"
description: "validator dive: валидация слайса go поэлементно — правила контейнера и элементов вокруг разделителя, unique, карты с keys и endkeys, ошибки по индексам."
course: "Валидация в Go: go-playground/validator"
url: "https://golangmaster.ru/courses/validator/lessons/08-srezy-i-dive/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# Срезы и карты: тег dive

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

Тег dive — единственный способ в validator добраться до каждого элемента среза или карты: правила до разделителя описывают контейнер, правила после — элементы по очереди. В этом уроке размечаем список тегов статьи: минимальная длина элемента, лимит количества, запрет дублей через unique, ключи и значения карты — и собираем ошибки с индексами, которые фронт подсвечивает построчно.

Списки — самый недопроверенный тип данных в API. Теги статей, позиции заказа, атрибуты товара: форма сверху выглядит валидной, а внутри среза живут пустая строка, дубль и элемент на пятьсот символов. Полиморфизм min и max из третьего урока здесь работает наполовину: на срезе они считают элементы, а не символы — «минимум 1» не спасёт от пустых строк внутри элементов. Для содержимого контейнера валидатор требует отдельного приказа, и этот приказ — dive: встретив его в теге, движок переключается с контейнера на элементы и прогоняет оставшиеся правила по каждому. Разделитель один, а последствий два: всё до него описывает контейнер, всё после — элементы. Это простое правило держит весь урок.

### validator dive: правила до и после разделителя

Соберём список тегов статьи: Title сверху и Tags []string — от нуля до десяти тегов, каждый от одного до шестнадцати символов. Тег поля собирается так: omitempty разрешает пустой список, max=10 ограничивает количество элементов, dive переключает режим, и дальше min=1,max=16 уже про каждый элемент. Валидация слайса go читается как предложение: контейнер — максимум десять элементов, внутри каждого — от одного до шестнадцати символов. Ключевая разница с третьим уроком в том, куда смотрит правило: min=1 до dive требует хотя бы один элемент в срезе, min=1 после dive требует непустую строку в каждом элементе — одинаковая запись, два разных договора. Правила после dive исполняются для каждого элемента слева направо до первого провала, ровно как на плоских полях; элемент не проходит одно правило — его проверка завершена, следующие не выполняются. Контейнер при этом проверяется один раз, до цикла. Массивы живут по тем же правилам: [4]string под dive проверит каждый из четырёх элементов, а len=4 до dive потребует ровно четыре. И последняя деталь грамматики: dive может стоять и последним правилом — тогда он просто включает проверку элементов, а правила к ним приезжают из тегов самих структур, как в срезе объектов из пятого блока. Так тегом из одной строки описывается конвейер, который ручным кодом занял бы вложенный цикл с ветками на каждый случай.

### Список тегов статьи: контейнер и элементы

```go
type ArticleForm struct {
        Title string   `validate:"required,min=5,max=120"`
        Tags  []string `validate:"omitempty,max=10,dive,min=1,max=16"`
}

// Tags: ["go", "", "очень длинный тег из многих слов"]
//   Namespace()          Field()  Tag()  Param()
//   ArticleForm.Tags[1]  Tags[1]  min    1
//   ArticleForm.Tags[2]  Tags[2]  max    16
```

max=10 стоит до dive — это правило контейнера, элементов три, проходит. После dive правило min=1 ловит пустую строку во втором элементе, max=16 — длинный тег в третьем. Ошибки приезжают отдельными записями с индексами в Namespace(), поэтому фронт подсвечивает конкретные строки списка, а не весь блок разом.

### Контейнер против элемента

| Правило | До dive (контейнер) | После dive (элемент) |
| --- | --- | --- |
| min=N | элементов ≥ N | длина элемента ≥ N рун |
| max=N | элементов ≤ N | длина элемента ≤ N рун |
| required | срез не nil — пустой, но инициализированный пройдёт | элемент — не нулевое значение |
| unique | нет дублей среди элементов | места нет: это правило контейнера |
| len=N | ровно N элементов | ровно N рун в элементе |
| email, oneof и другие | — | формат каждого элемента |

### Проверка: куда смотрит min

**Вопрос:** Tags []string с тегом max=10,dive,min=1. Срез из двенадцати непустых коротких элементов. Что вернёт валидатор?

- nil: элементы в норме, а min=1 про пустые строки
- ✅ Одну ошибку по Tags с тегом max: элементов больше десяти
- Двенадцать ошибок по min=1
- Ошибку по dive: срез длиннее лимита разделителя

max=10 стоит до dive и описывает контейнер: двенадцать элементов больше лимита — одна запись с тегом max. min=1 после dive не сработал: до элементов дело не дошло, контейнер провалился раньше. dive — не тег-проверка, а разделитель правил, у него нет собственного вердикта.

### валидация слайса go: ошибки по индексам

Элементы независимы, как поля плоской структуры: один провалился — проверка остальных продолжается, и каждая ошибка становится отдельной записью ValidationErrors. В поле Field() такой записи появляется индекс: Tags[1], Tags[2] — это обещание пятого урока сбылось. Именно индексы превращают технический список в человеческий ответ: каталог сообщений из шестого урока строит «тег №2 пустой» по Field(), и фронт подсвечивает конкретную строку списка, а не блок целиком. Отсюда два практических вывода. Первый: не склеивайте ошибки элементов в одну строку на бэкенде — держите по записи на элемент, формату Details из двадцать четвёртого урока это подходит идеально. Второй: ожидайте несколько записей по одному полю — срез из трёх пустых элементов даст три записи с разными индексами, и тесты должны это покрывать. Сообщение при этом собирается из готовых деталей: Field() дал «Tags[1]», каталог по Tag() вернул «пусто нельзя, минимум 1 символ» — и пользователь видит «Тег 2: пусто нельзя», не зная ничего о срезах и нумерации с нуля. Отдельная тонкость порядка: внутри одного элемента работает правило «первый провалившийся тег завершает проверку», поэтому длинный пустой элемент вернёт min, а не max — как у Password в пятом уроке. И последнее: индексы в Namespace() — материал для ретрая и логов, но не для ответа клиенту как есть: пользователь думает строками формы, а не номерами массива.

### Сколько записей и с какими индексами

```go
type Playlist struct {
        Names []string `validate:"max=5,dive,min=2"`
}

p := Playlist{Names: []string{"go", "x"}}
err := validate.Struct(&p)
// что лежит в ValidationErrors?
```

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

- nil: «go» проходит, а «x» — не пустая строка
- ✅ Одна запись: Names[1] с тегом min, Param «2»
- Одна запись: Names с тегом min — правило проверило контейнер
- Две записи: Names[0] и Names[1] обе по min=2

min=2 стоит после dive и проверяет длину каждого элемента: «go» — две руны, проходит; «x» — одна, проваливает. Одна запись с индексом Names[1]. Контейнерное max=5 молчит: элементов два. Ошибки по «go» нет — правила исполняются слева направо, и одна руна проваливает min раньше прочего.

### unique: список без дублей

Второй список-специфический тег — unique: он требует, чтобы элементы не повторялись. Правило уровня контейнера, поэтому ставится до dive, вместе с max и len: validate:"omitempty,unique,dive,min=1,max=16". Сценарии — теги статьи, список адресатов, набор купонов: дубль здесь не ошибка формата, а логическая ерунда, которую не поймает ни один тег формата из четвёртого урока. Механика у unique простая: валидатор собирает элементы в служебную карту и ловит повтор — значит, элементы должны быть сравнимыми: строки, числа, структуры без срезов и карт внутри. Срез несравнимого типа под unique приведёт к панике в рантайме при первой же проверке — тот же жанр, что битый тег из первого урока, и лечится тем же лекарством: тестом с реальным значением на старте. Для срезов структур, где «уникальность» означает «нет двух позиций с одним артикулом», у unique есть параметр: unique=SKU сравнивает элементы по полю SKU и ловит повтор (с версии v10.12), а на отдельном поле той же механикой работает сравнение с соседом — «дата конца не равна дате начала». Ошибётесь в имени поля — паника «Bad field name» на первой проверке, и это тоже ловит тест на старте. Уникальность по нескольким полям сразу — по-прежнему территория StructLevel из одиннадцатого урока. И заметьте разделение труда: unique следит за взаимностью элементов, dive — за качеством каждого по отдельности; одно другому не заменяет, и в честных формах оба правила живут рядом. Комбинация с oneof внутри дива закрывает и качество, и состав: omitempty,unique,dive,oneof=go rust python — список без дублей, где каждый элемент из перечня. Одна строка тега заменяет три ветки кода, и ошибки по-прежнему едут с индексами.

> **Живое место: min=1 повесили не на тот уровень.** Команда хотела «в списке нет пустых строк» и написала min=1 перед dive. Правило прилипло к контейнеру и стало требовать хотя бы один элемент: пустой срез проваливается («минимум 1 элемент» у пользователя, который просто не добавил теги), а срез из пустых строк проходит насквозь — ровно наоборот от задуманного. Симптом неявный: тесты с заполненными списками зелёные, прод ломается на пустой форме. Лечится чтением тега вслух: всё до dive — про контейнер, всё после — про элементы. Проверяйте пятёркой значений из третьего урока: пустой срез, один элемент, пустая строка внутри, дубль, длинный элемент.

### Карты: dive, keys и endkeys

С картами dive работает по той же схеме с одной надстройкой: после dive правила применяются к значениям, а для ключей есть собственный сэндвич — между тегами keys и endkeys пишутся правила ключа, после endkeys — правила значения. Атрибуты товара: Attrs map[string]string, ключ — имя атрибута от одного до двадцати символов, значение — до двухсот. Один тег описывает обе стороны каждой пары, и ошибки различают, что именно сломалось: путь в Namespace() содержит сам ключ карты, поэтому из ответа видно, какая пара виновата. Пустая карта под dive не проверяет ничего — итераций ноль, это не ошибка; обязательность наличия пар описывается required или min=1 до dive, по правилам контейнера. Для структур в значениях действует то же правило, что и для срезов структур: dive включает рекурсию седьмого урока, и путь до ошибки проходит сквозь ключ карты. Практический совет: не описывайте в ключах сложные форматы — ключ карты чаще всего едет в JSON-объект, где его формат договорён с фронтом; тегу оставьте длину и oneof на перечень допустимых имён атрибутов. И держите сэндвич в голове как целое: keys пишут в паре с endkeys, чтобы границы правил ключа и значения в теге оставались читаемыми — вольная перестановка превращает тег в загадку для следующего ревью.

### Атрибуты товара: ключи и значения в одном теге

```go
type MetaForm struct {
        Attrs map[string]string `validate:"omitempty,dive,keys,min=1,max=20,endkeys,max=200"`
}

// Attrs: {"цвет": "красный", "": "потерянный ключ"}
// ключ ""   → провал keys-правила min=1: путь содержит сам ключ
// значения  → max=200 после endkeys, по каждой паре
```

Правила между keys и endkeys — про ключ: min=1 отвергает пустое имя атрибута, max=20 ограничивает его длину. После endkeys идут правила значения: max=200 на каждую пару. omitempty разрешает карту без атрибутов. В путях ошибок фигурирует сам ключ карты, поэтому виновника видно без угадываний по индексам.

### Проверка: чьё правило между keys и endkeys

**Вопрос:** В теге карты написано dive,keys,min=1,endkeys,max=200. К чему применяется min=1?

- К значениям: каждое значение не пустое
- ✅ К ключам: каждый ключ от одного символа
- К контейнеру: в карте минимум одна пара
- И к ключам, и к значениям одновременно

Сэндвич работает буквально: между keys и endkeys живут правила ключа, после endkeys — правила значения. min=1 на ключе отвергает пустое имя атрибута; max=200 ограничивает значение. Минимальное количество пар — это required или min на контейнере до dive.

### Срез структур: dive внутрь полей

Самый частый в реальных API случай — срез не строк, а структур: Positions []Item в заказе, Addresses []Address в профиле. Здесь dive включается в рекурсию из прошлого урока: правило после dive применяется к каждому элементу, элемент — структура, и валидатор спускается в её поля по их тегам. Пути ошибок собирают всё сразу: Order.Positions[0].Sku, Order.Positions[2].Qty — индекс элемента плюс маршрут внутри него, и фронт подсвечивает конкретную ячейку конкретной строки. Для указателей в срезе работает договор прошлой недели: []*Address с dive пропустит nil-элементы без спуска, а required на элементе потребует ненулевой указатель в каждой позиции. Условные теги из следующего урока тоже работают внутри элементов: требование «у позиции со скидкой обязателен код причины» выражается тегом на поле структуры, и dive доставит его до каждой строки. Единственное, чего dive не делает, — сравнивает элементы между собой: «сумма позиций равна итогу» и «артикулы не повторяются» живут за пределами тегов, в StructLevel из одиннадцатого урока. На практике здесь любят гранулярные Details: ошибка «позиция 3: количество больше остатка на складе» читается оператором быстрее, чем общее «проверьте позиции», — формат ошибок API рассчитан именно на такой по-элементный список.

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

- всё до dive — правила контейнера, всё после — правила элементов
- мин/max до dive считают элементы, после — руны строки или поля структуры
- элементы независимы: три плохих элемента — три записи с индексами в Field()
- внутри элемента работает «первый провалившийся тег завершает проверку»
- unique — правило контейнера, ставится до dive; элементы обязаны быть сравнимыми
- для карты: правила ключа — между keys и endkeys, значения — после
- пустой контейнер под dive не проверяет ничего; обязательность — required или min до dive
- для срезов структур dive включает рекурсию прошлых уроков, пути получают индексы

> **Спорное: dive на больших срезах.** Поэлементная проверка через reflect заметно дороже плоской структуры, и на горячих путях я не ставлю dive с тяжёлыми правилами на срезы в тысячи элементов — импорт каталога, пакетная загрузка, вебхуки с большим телом. Там честнее ручной проход обычными if с накоплением ошибок: быстрее, и ошибки собираются по своей логике. Условие, при котором dive прав везде: элементов десятки, источник — пользователь, а требование «подсветить каждую строку формы» важнее наносекунд. Цена отражения и способы её измерить — отдельная тема [урока про производительность](/courses/validator/lessons/20-proizvoditelnost/).

### Соберите теги списка и карты

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

```go
type ArticleForm struct {
        Tags  []string          `validate:"omitempty,max=10,___,min=1,___"`
        Attrs map[string]string `validate:"dive,___,min=1,___,max=200"`
}
```

Подсказка: Переключение на элементы — dive; без дублей — unique; начало правил ключа — keys, конец — endkeys.

Правильные ответы в пропусках: `dive`, `unique`, `keys`, `endkeys`

### Соберите путь списка через валидатор

Клиент отправил статью с тремя тегами, один из них пустой. Разложите события по порядку.

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

1. `Клиент отправляет статью с тегами: "go", "", "вёрстка"`
2. `Хендлер декодирует JSON в ArticleForm`
3. `omitempty видит непустой срез — проверка продолжается`
4. `max=10 проверяет контейнер: три элемента — меньше лимита`
5. `dive переключает оставшиеся правила на элементы`
6. `Пустой элемент проваливает min=1, остальные проходят`
7. `Field() возвращает индекс: Tags[1]`
8. `Каталог собирает «тег №2 пустой», ответ 400 уходит клиенту`

### Карточки: контейнеры и элементы

- **dive** — Разделитель правил: до него — контейнер (количество элементов), после — каждый элемент по очереди. Не проверка, а переключатель режима.
- **min до dive и после** — До dive — минимум элементов в срезе; после — минимальная длина каждого элемента. Одна запись, два разных договора — источник частых багов.
- **unique** — Правило контейнера против дублей: собирает элементы в служебную карту. Элементы должны быть сравнимыми; для срезов структур есть параметр unique=Поле — сравнение по одному полю, сложнее — StructLevel.
- **keys и endkeys** — Сэндвич для карт: правила между ними применяются к ключам, после endkeys — к значениям. Пустая карта под dive не проверяется вовсе.
- **Field() у элемента** — Возвращает имя с индексом: Tags[1]; для карты в пути фигурирует сам ключ. Материал для сообщений «тег №2 пустой» и подсветки строк формы.
- **dive на срезе структур** — Включает рекурсию в поля каждого элемента; пути получают индексы: Positions[0].Sku. nil-элементы в []*T пропускаются без required.

Девятый урок — [условные теги: required_without, excluded_with](/courses/validator/lessons/09-uslovnye-tegi/): зависимости полей внутри формы и внутри элементов среза, который мы сегодня научились доставать dive-ом. Полиморфизм длин и границ, на котором построен весь урок, разобран в [третьем уроке](/courses/validator/lessons/03-tegi-required-min-max/), рекурсия для срезов структур — в [седьмом](/courses/validator/lessons/07-vlozhennye-struktury/). Код формы статьи соберите локально: песочница сайта исполняет только чистый Go без внешних модулей, поэтому валидатор — программа вашего терминала, а интерактивы закрепляют уровни правил без запуска.

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

- dive — разделитель: до него правила контейнера, после — правила каждого элемента.
- min и max до dive считают элементы, после — руны строк; одна запись, два разных договора.
- Элементы независимы: три плохих элемента дают три записи FieldError с индексами вроде Tags[1].
- unique — правило контейнера против дублей, ставится до dive; элементы должны быть сравнимыми.
- Для карт: правила ключей — между keys и endkeys, значения — после; в пути ошибки виден сам ключ.
- Для срезов структур dive включает рекурсию, и пути получают индексы: Positions[0].Sku.
- На больших срезах горячего пути dive с тяжёлыми правилами заменяют ручным проходом — цена отражения.

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

Возьмите форму статьи и доведите список до контракта: Tags []string с omitempty,max=10,unique,dive,min=1,max=16. Прогоните локально пять случаев: пустой срез, один тег, пустая строка внутри, два одинаковых тега, тег на двадцать символов. Запишите, какие записи легли в ValidationErrors и что показал Field() для каждой. Потом переставьте min=1 перед dive и найдите случай, при котором поведение ломается ровно наоборот от задуманного — вы только что воспроизвели главную граблю урока.

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

### Почему пустой срез проходит валидацию с dive?

Потому что dive — цикл по элементам, а в пустом срезе итераций ноль: нечего проверять. Обязательность выражается правилами контейнера: min=1 до dive требует хотя бы один элемент, а required отфильтрует только nil — пустой, но инициализированный срез он пропускает, что после декодирования JSON означает «поле было, но с пустым массивом». Если пустой список легитимен, оставьте omitempty — тогда отсутствие и пустота пропускаются честно.

### Как проверить, что элементы среза принадлежат перечню значений?

Поставьте oneof после dive: правила формата применяются к каждому элементу. oneof=go rust python после dive отвергнет элемент вне перечня с индексом в Field(). Перечень с пробелами внутри тега и запятыми между правилами — грамматика из третьего урока работает и здесь.

### unique провалился — можно ли сказать, какие элементы дублируются?

Нет: unique — проверка уровня контейнера, и запись в ValidationErrors одна, без индексов виновников. Если продукт требует назвать дубли, проверяйте пересечения сами и складывайте результат в ответ — или заворачивайте в StructLevel-правило одиннадцатого урока, где доступен весь срез.
