Содержание урока · 10 разделов
- validator required tag: не-нулевое, а не «заполненное»
- min max валидация: три смысла одного тега
- Поведение тега по типам поля
- omitempty: компаньон опциональных полей
- Точные границы: len, eq, ne, gt и lt
- Один тег — цепочка правил через запятую
- Частые грабли required и min/max
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Скелет формы уже готов с прошлого урока: структура, конструктор валидатора, вызов Struct. Теперь наполняем правила. Наша форма профиля — ProfileForm с четырьмя полями: Username, Password, Age и необязательный About. Для каждого поля зададим комбинацию обязательности и границ, а попутно разберём главный сюрприз этой триады: required проверяет не «заполнено ли», а «не нулевое ли значение», и именно на этом зазоре живут самые частые баги новичков. Диапазоны тоже не так просты, как выглядят: min и max меняют смысл в зависимости от типа поля, и это надо выучить один раз — на всю карьеру с валидатором. План урока такой: сначала required и его зазор между «нулевым» и «заполненным», затем полиморфизм min и max с таблицей по типам, дальше omitempty и указатели как два способа сказать «поле необязательное». Каждому тегу — своя грабля из практики, и к концу урока вы соберёте чек-лист правил, которым будете пользоваться во всех следующих формах курса.
validator required tag: не-нулевое, а не «заполненное»#
required звучит как «поле заполнено», но реализовано строго: поле не должно быть нулевым значением своего типа. Для строк это пустая строка — и только: строка из пробелов required пройдёт, потому что она не пустая. Для чисел — ноль, для bool — false, для указателей и интерфейсов — nil, для срезов и карт — nil или нулевая длина. Отсюда первая крупная ловушка курса: числовое поле, где ноль — легитимное значение. Количество детей, этаж, скидка в процентах — во всех этих случаях required отвергнет честный ноль, и пользователь формы получит «обязательное поле», хотя ничего не забыл. То же самое с деньгами: сумма перевода 0 на int с required уедет в отказ, хотя пользователь мог ввести ноль осознанно — перевод между своими счетами без комиссии. Различать «не указано» и «указан ноль» можно только указателем: *int с required,gte=0 пропустит честный ноль и отвергнет отсутствующее значение. Решений два: пересмотреть, действительно ли ноль допустим (если нет — нужен диапазон вроде gte=1), либо перейти на указатель, где nil и ноль различимы. И вторая ловушка — строка из пробелов: required её пропустит, а бизнес примет «имя пользователя» из одних пробелов. Отсюда правило: чистка пробелов — это нормализация перед валидацией, у неё отдельный урок курса. Третье следствие из семантики required — пустые контейнеры: срез тегов длиной ноль required отвергнет так же, как ноль числа; если пустой список легитимен, поле описывается парой omitempty,min=1 — «пусто можно, непусто хотя бы один элемент».
Форма профиля: четыре профиля правил
GOtype ProfileForm struct {
Username string `validate:"required,min=3,max=32"`
Password string `validate:"required,min=8,max=72"`
Age *int `validate:"required,gte=14"`
About string `validate:"omitempty,max=500"`
}
// Три поля обязательны, About — нет.
if err := validate.Struct(&form); err != nil {
// ValidationErrors: по записи на каждое сломанное поле
}Четыре поля — четыре рабочих комбинации. Username и Password обязательны с диапазоном длины; Age — указатель с required: nil означает «не указан», а не нулевой возраст, и диапазон gte проверяется по значению после разыменования; About необязателен, но непустой — не длиннее пятисот символов. Каждую комбинацию разберём по отдельности ниже.
min max валидация: три смысла одного тега#
min и max — самые полиморфные теги валидатора: их смысл зависит от типа поля. Для строк они считают длину, причём в рунах, а не в байтах: «привет» — семь символов, хотя байтов двенадцать; для кириллических форм это принципиально, потому что len() по байтам дал бы вдвое больше. Для чисел min и max сравнивают само значение: min=1 на int отвергает ноль и отрицательные числа. Для срезов, карт и массивов — количество элементов: min=1 на []string требует хотя бы один элемент. Границы у min/max включительные: значение ровно на границе проходит проверку. Если нужна строгая граница, берите gt и lt, а нестрогие аналоги gte и lte для чисел работают как «минимально/максимально допустимое». Точный размер даёт len=13 — например, для строк фиксированной длины вроде штрихкодов и кодов подтверждения. На практике полиморфизм стреляет вот где: команда пишет max=140, думая о лимите твита, а поле — заголовок задачи в символах; для латиницы лимит совпадает с подсчётом фронтенда, для кириллицы — совпадает тоже (руны везде), а вот если фронт считает байтами — начинается спор про «пятьдесят символов» против «сотни байтов». Договаривайтесь на рунах и фиксируйте это в доках API. Таблица ниже собирает всю матрицу «тег против типа» на один экран — вернитесь к ней, когда сомневаетесь.
Поведение тега по типам поля#
| Тег | Строка | Число | Срез/карта | Указатель |
|---|---|---|---|---|
| required | не пустая | не 0 | не nil и не пустой | не nil |
| min=N | длина ≥ N рун | значение ≥ N | элементов ≥ N | правило по типу за разыменованием |
| max=N | длина ≤ N рун | значение ≤ N | элементов ≤ N | правило по типу за разыменованием |
| gt=N / lt=N | — | строго больше / меньше | — | правило по типу за разыменованием |
| len=N | ровно N рун | — | ровно N элементов | — |
| omitempty | нулевой — остальные правила пропускаются | то же | то же | nil пропускается |
Проверка: руны или байты
Строка «Йо» и тег min=3. Пройдёт ли проверка?
omitempty: компаньон опциональных полей#
Необязательное поле — не то же самое, что поле без правил. Поставили min=3 на необязательное поле — и пустая строка провалится: min проверяет длину, а нулевая длина меньше трёх. omitempty решает конфликт: если поле нулевое значение своего типа, все остальные правила тега пропускаются. Комбинация omitempty,min=3,max=500 читается как договор: пусто — можно; непусто — от трёх до пятисот символов. Порядок правил в теге роли не играет — omitempty может стоять и в конце, — но конвенция ставить его первым улучшает читаемость. Частая ошибка новичка противоположна: привычка вешать omitempty на всё подряд, включая обязательные поля. required и omitempty в одном теге противоречат друг другу: required отвергает нулевое, которое omitempty только что пропустил, — поведение получается запутанным и зависит от порядка исполнения правил. Выбирайте одно из двух: поле обязательное или нет. Отдельный кейс для omitempty — PATCH-обновления: клиент присылает только изменённые поля, остальное отсутствует, и после декодирования структура заполнена нулями. Без omitempty валидация всей структуры отвергла бы такие запросы пачкой; с omitempty на необязательных полях и точечным перечислением обязательных через StructPartial — метод из прошлого урока — частичные обновления описываются без отдельной структуры на каждый эндпоинт. И ещё разница, о которой забывают: поле, отсутствующее в JSON, и поле с пустой строкой после декодирования для валидатора неразличимы — оба нулевые. Поэтому omitempty честно описывает оба случая сразу, а вот проверка «пользователь прислал, но пустое» тегами не решается — нужны указатели или приёмы из урока про DTO-границы.
Типичная ошибка: Age int с тегом min=13 вместо required,gte=13. Клиент не прислал поле вовсе — после декодирования там ноль — и валидатор отвечает тегом min, хотя пользователь ничего не указывал. Сообщение выходит бессмысленным («слишком мало» для поля, которого нет), а фронтенд подсвечивает инпут, который пользователь не трогал. Обязательность и диапазон — разные правила: required говорит «укажите», min, max, gte и lte — «укажите в этих пределах».
Указатели: nil, ноль и «не указано»
GO// nil и «не указано» — разные вещи, и указатель это различает.
type MeetingForm struct {
Attendees *int `validate:"required,gte=2,lte=50"`
}
// {"attendees":null} → required: nil-указатель, значения нет
// {"attendees":0} → gte=2: число указано, но 2..50 не попало
// {"attendees":12} → прошло: 12 в диапазонеНа указателе required означает «не nil»: поле обязали указать, а ноль внутри — конкретное значение, которое проверит gte. Без указателя int не различил бы «не прислали» и «прислали ноль» — после декодирования оба выглядят нулём. Бонус связки: после успешной проверки Attendees гарантированно не nil, и разыменование в бизнес-коде безопасно.
Точные границы: len, eq, ne, gt и lt#
Между «от» и «до» живёт семья точных проверок. len=N требует точного размера: строка из тринадцати символов для штрихкода, срез ровно из шести цифр кода подтверждения. eq и ne сравнивают значение с константой: eq=0 удобен для «поле должно быть нулём» в статусных структурах, ne — для запрета конкретных значений, хотя чаще список запретов переворачивают в oneof с перечнем допустимых. Для чисел полезна пара gt и lt — строгие границы: gt=0 исключает ноль там, где min=0 его пропускает, и это спасает от очередного бага «минимальное количество — ноль». Все эти теги подчиняются той же логике типов: строки считают руны, числа сравнивают значения, срезы — количество элементов. И так же, как min и max, они не прощают нулевых значений без omitempty: точная проверка на необязательном поле требует пары с omitempty, иначе пустая строка провалит len=6. Границы честнее проверять на граничных значениях: min=3 пропускает ровно три символа и ровно три элемента — тест со строкой из трёх символов скажет о правиле больше, чем чтение документации. Если поведение тега кажется неочевидным, соберите его локально на пяти значениях: ниже границы, на границе, выше, нулевое и пустое — эта пятёрка вскрывает любой полиморфный тег быстрее, чем чтение чужих туториалов.
Что вернёт проверка формы
Прочитайте код и выберите, что он выведет:
type TicketForm struct {
Promo string `validate:"omitempty,min=4,max=16"`
Seats int `validate:"min=1,max=6"`
}
in := TicketForm{Promo: "", Seats: 1}
err := validate.Struct(&in)
// что в err?Один тег — цепочка правил через запятую#
Внутри тега правила разделяются запятыми и исполняются слева направо до первого провала: validate:"required,min=3,max=32" — сначала required, потом min, потом max; споткнувшись, поле завершает проверку, и остальные правила не выполняются. Параметр правила идёт через знак равенства: min=3. Пробелы вокруг правил — источник тихих сюрпризов: пробел после запятой превращает min=3 в неизвестный тег с именем « min», и валидатор паникует Undefined validation function на первом же прогоне структуры — форматируйте теги без пробелов. Перечень допустимых значений задаёт oneof, где значения разделяются уже пробелами: oneof=admin editor viewer — внутри одного тега грамматика двойная, запятые между правилами, пробелы внутри перечня. Форматы вроде email и datetime разобраны в следующем уроке, а здесь напомним про нормализацию: правила работают с тем, что им дали, — trim и нижний регистр делайте до validate.Struct, иначе « a@b.ru » придётся описывать тегами на каждый случай.
Частые грабли required и min/max#
- min на строке считает руны, а не байты — не сверяйте его с len()
- required на числе отвергает честный ноль — если ноль легитимен, берите указатель или omitempty
- omitempty и required в одном теге противоречат друг другу — выберите одно
- пробел после запятой в теге — неизвестный тег и паника Undefined validation function
- min и max включительные: для строгой границы нужны gt и lt
- min на срезе требует элементы, а не символы внутри строк — для элементов есть dive
Проверка: required на булевом поле
Поле Confirmed bool с тегом required. Пользователь не поставил галочку. Что вернёт валидатор?
Спорное мнение с условием: не ставьте required по умолчанию. Требуйте только то, без чего логика не работает, — остальным достаточно omitempty с диапазоном. Сценарий поломки: API добавляет обязательное поле — и мобильное приложение прошлого релиза, которое о нём не знает, падает с 400 на каждом запросе. Обязательное поле — коммит перед всеми потребителями API; поле новой фичи чаще должно быть опциональным, пока старые клиенты не обновлены.
Соберите правила формы
Заполните пропуски: обязательность имени, опциональность описания, нижняя граница возраста и точная длина кода.
Соберите путь формы через валидатор
Клиент отправил имя из одной буквы и не указал возраст. Разложите события по порядку.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: триада тегов
Следующий урок — форматы: email, url, uuid и datetime: длины и диапазоны разобрали, переходим к проверке синтаксиса значений без ручных регулярных выражений. Ошибки, которые возвращают эти теги, разбираем в пятом уроке — методы Field, Tag и Param превращают провал min в понятный ответ пользователю. Сообщения на русском собираются в шестом уроке, а чистка пробелов перед проверками живёт в отдельном уроке про нормализацию. Код формы соберите локально: песочница сайта исполняет только чистый Go без внешних модулей, поэтому валидатор живёт в вашем терминале, а интерактивы закрепляют правила без запуска.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- required — не-нулевое значение типа: пустая строка, 0, false, nil, пустой срез; честный ноль он не отличит от «не указано».
- min и max полиморфны: руны у строк, значение у чисел, элементы у срезов; границы включительные, строгие — gt и lt.
- min для строк считает руны, а не байты: кириллица не удваивает длину, len() — удваивает.
- omitempty пропускает остальные правила на нулевом поле; в паре с required противоречит сам себе.
- Указатель на число различает «не указано» и ноль; required на *T — это «не nil».
- Правила в теге идут через запятую до первого провала; пробел после запятой — паника Undefined validation function.
Домашнее задание
Возьмите форму из урока и добавьте два поля: Promo string с omitempty,min=4,max=16 и Seats *int с required,gte=1,lte=6. Прогоните локально четыре случая: оба заполнены валидно, Promo пуст, Seats = 0, Seats = nil. Запишите, какие теги сработали в каждом случае, и объясните себе, почему Seats = 0 и Seats = nil дали разные ошибки.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему required отвергает 0, хотя это легитимное значение?
required проверяет нулевое значение типа, а ноль числа — нулевое значение. Если ноль допустим по смыслу, различите «не указано» и ноль указателем *T, либо разрешите пустоту omitempty и проверяйте диапазон только при наличии значения. Оба приёма разобраны в уроке выше — на Age и About.
min=3 считает байты или символы?
Символы (руны). «Йо» — две руны и четыре байта, min=3 её не пропустит. Правила для строк считают длину в рунах, поэтому кириллица не даёт двойную длину, как это делает len(). Если фронтенд считает лимиты по-своему, разногласия вскрываются именно на кириллице — сверяйте подсчёт и фиксируйте правило в доках API.
Можно ли совместить required и omitempty?
Не стоит: omitempty пропускает правила на нулевом поле, а required требует ненулевое — конфликт тегов делает поведение запутанным. Выберите одно: поле обязательное (required) или необязательное (omitempty плюс правила).
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Asynq в Go: очередь фоновых задач на Redis
Курс asynq для Go: клиенты и воркеры, отложенные и периодические задачи, ретраи, очереди с приоритетами, агрегация, Inspector и asynqmon. 25 уроков.
- Строить клиент и воркер asynq и подключать Redis
- Отправлять отложенные, периодические и уникальные задачи
- Настраивать ретраи, таймауты и приоритеты очередей
- Мониторить задачи через Inspector и asynqmon