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

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

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

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

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

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

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

Списки — самый недопроверенный тип данных в 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. Срез из двенадцати непустых коротких элементов. Что вернёт валидатор?

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

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

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

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

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

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

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 — список без дублей, где каждый элемент из перечня. Одна строка тега заменяет три ветки кода, и ошибки по-прежнему едут с индексами.

Карты: 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?

Срез структур: 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 включает рекурсию прошлых уроков, пути получают индексы

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

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

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

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

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

Строки

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

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

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

Термин

dive

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

Разделитель правил: до него — контейнер (количество элементов), после — каждый элемент по очереди. Не проверка, а переключатель режима.

1 / 6

Девятый урок — условные теги: required_without, excluded_with: зависимости полей внутри формы и внутри элементов среза, который мы сегодня научились доставать dive-ом. Полиморфизм длин и границ, на котором построен весь урок, разобран в третьем уроке, рекурсия для срезов структур — в седьмом. Код формы статьи соберите локально: песочница сайта исполняет только чистый Go без внешних модулей, поэтому валидатор — программа вашего терминала, а интерактивы закрепляют уровни правил без запуска.

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

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

Вывод

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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