Содержание урока · 10 разделов
- go-playground validator установка: go get
- validator.New: что происходит при создании
- nil против ошибок: контракт Struct()
- Первый запуск: что вы увидите на экране
- Var: проверка одного значения
- Методы *Validate, которые пригодятся сразу
- Четыре грабли на первой неделе
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Понадобится локальный Go: валидатор — обычная библиотека из модуля, а браузерная песочница сайта исполняет только чистый Go без внешних зависимостей, поэтому её не загрузит. Дальше — новый каталог, терминал и три минуты. Мы соберём минимальную программу: структура с тегами, конструктор валидатора, вызов проверки и печать результата. Каждую строку разберём по отдельности, а к концу урока вы будете знать, что возвращает вызов Struct во всех трёх случаях — успех, провал правил и переданный nil. Если вы уже работали с Gin или Echo, половина урока покажется знакомой: фреймворки прячут ровно эти шаги под капотом. И одна оговорка про тон курса: мы не спешим. Первая программа — фундамент, на котором дальше стоят теги, сообщения об ошибках и интеграции, поэтому каждая строка здесь получит объяснение, а не только "скопируйте и запустите". Если хочется быстрее — не пропускайте блок про контракт nil против ошибок: его недопонимание даёт половину багов у новичков валидатора.
go-playground validator установка: go get#
Установка validator — одна команда go get в каталоге с go.mod. Если модуля ещё нет, сначала go mod init demo: современные версии Go не подтягивают зависимости молча. Путь импорта включает мажорную версию: github.com/go-playground/validator/v10 — так требуют Go-модули для версий со второй и выше, поэтому «голый» импорт без /v10 не скомпилируется. Пакет, несмотря на длинный путь, называется просто validator: в коде вы пишете validator.New(), а не playground.New(). Девятая версия — легаси с другим набором функций и без развития; в старых туториалах встречается путь github.com/go-playground/validator без суффикса — это она и есть, не перепутайте. После go get в go.mod появится строка require с версией вида v10.x.x: минорные обновления внутри десятой ветки ставятся через go get -u и не ломают код — API v10 стабилен годами. Про vendor и CI: если проект собирается в изолированном окружении без сети, добавьте зависимость в каталог vendor через go mod vendor или положитесь на кэш модулей в пайплайне — валидатор чистая библиотека без транзитивных зависимостей, поэтому весит неприлично мало. И проверьте версию Go в go.mod: библиотека требует не самый свежий Go, но новые версии языка иногда открывают в ней новые опции.
Терминал: новый модуль и зависимость
GO// Новый модуль, если ещё не создан:
go mod init demo
// Установка валидатора:
go get github.com/go-playground/validator/v10
// В go.mod появится запись (версия может быть новее):
// module demo
//
// go 1.22
//
// require github.com/go-playground/validator/v10 v10.19.0Две команды — и зависимость в проекте: init создаёт модуль, get скачивает библиотеку в кэш модулей и прописывает require. Мажорная версия v10 — часть пути импорта; в коде импортируйте ровно этот путь, тогда пакет по-прежнему зовётся validator. Версия в комментарии — иллюстрация: свежая может быть выше, API внутри десятой ветки стабилен.
validator.New: что происходит при создании#
validate := validator.New() — короткая строка с солидным содержимым. Конструктор возвращает *Validate — объект, который при первом прогоне каждой структуры парсит её теги через reflect и складывает план проверки во внутренний кэш. Кэш индексируется по reflect.Type, поэтому стоимость разбора платится один раз на каждую структуру за жизнь процесса: тысяча разных структур — тысяча разборов, но не тысяча на каждый запрос. Повторные вызовы Struct на той же структуре работают по готовому плану, без повторного отражения. Первые проверки после старта медленнее последующих: планы собираются лениво, при первом появлении структуры, — если сервис чувствителен к задержкам, прогрейте валидатор на старте, прогнав основные структуры через Struct с тестовыми значениями, иначе первый пользователь оплатит разогрев. Экземпляр потокобезопасен для проверок — параллельные запросы спокойно делят один валидатор, — а вот регистрация новых функций и алиасов через RegisterValidation и RegisterAlias потокобезопасной не является: её делают на старте, до первых проверок, иначе конкурирующая запись в реестр во время параллельной валидации — гонка, которую поймает не каждый тест. Из этого следует правило курса: валидатор создаётся один раз на приложение — пакетной переменной или полем сервиса. Если вы пришли из Gin, там ровно та же схема — один движок на весь фреймворк с переключённым словарём тегов, как мы разбирали в уроке про валидацию в Gin.
Первая валидация: SignupForm целиком
GOpackage main
import (
"fmt"
"github.com/go-playground/validator/v10"
)
type SignupForm struct {
Email string `validate:"required,email"`
Password string `validate:"required,min=8"`
Age int `validate:"gte=0,lte=130"`
}
func main() {
validate := validator.New()
form := SignupForm{Email: "не-почта", Password: "короткий", Age: 30}
err := validate.Struct(&form)
if err != nil {
fmt.Println("проверка не прошла:", err)
return
}
fmt.Println("всё чисто")
}Минимальная программа валидации: импорт по пути с /v10, структура с тегами, конструктор и вызов Struct. В form два поля заведомо сломаны — email не формат почты, пароль короче восьми символов, — поэтому в консоль уйдут две записи, а Age пройдёт. Поменяйте значения на валидные и увидите «всё чисто»: код тот же, маршрутов два.
nil против ошибок: контракт Struct()#
Контракт вызова простой, но двухслойный. Если всё прошло — Struct возвращает nil, и код не делает ничего: err != nil — единственная проверка, которая нужна в счастливом пути. Если правила не выполнились — возвращается validator.ValidationErrors, срез FieldError: одна запись на провалившееся поле с именем, тегом и параметром; структуру разберём по косточкам в уроке про ValidationErrors. Третий случай особый: если в Struct передали не структуру — nil, число, срез — возвращается validator.InvalidValidationError. Типы разные, и обрабатывают их по-разному: ValidationErrors — нормальный рабочий сценарий, InvalidValidationError — ошибка программиста, её пишут в лог или паникуют на старте, но не отдают пользователю. И последний сюрприз: неизвестный или битый тег не возвращается ошибкой — он паникует при первом прогоне структуры. Опечатка в validate:"requred" обнаружится не компилятором, а на живом вызове — тест с валидным значением обязателен. Практическая схема обработки в хендлере выглядит так: сначала errors.As на validator.InvalidValidationError — если сработало, это баг кода, а не данные; затем errors.As на validator.ValidationErrors — рабочий отказ, собираем ответ по полям; всё остальное — неожиданное, уходим в общий обработчик ошибок. Таких три ветки достаточно для девяноста девяти процентов проектов.
Проверка: счастливый путь
Что вернёт validate.Struct(&form), если все поля прошли проверку?
Что напечатает программа
Прочитайте код и выберите, что он выведет:
type SignupForm struct {
Email string `validate:"required,email"`
}
form := SignupForm{Email: "не-почта"}
err := validate.Struct(&form)
fmt.Println(err)
// что выйдет на экран?Первый запуск: что вы увидите на экране#
go mod init и go get из блока выше, сохранить main.go — и go run main.go в терминале. На печать выйдут две строки, начинающиеся с Key: 'SignupForm.Email' Error:Field validation... и запись про Password. Это дефолтный формат ValidationErrors: подробный, английский и людям не предназначенный — мы его приручим в уроке про русские сообщения, где соберём каталог понятных текстов, и в уроке про единый формат ошибок API, где он станет вежливым 400-ответом. Для практики здесь важнее другое: запустите программу, поменяйте значения полей и убедитесь, что набор записей в выводе меняется вместе с данными — это самое быстрое закрепление контракта из прошлого блока. Заодно прочувствуйте цену первого вызова: разбор тегов через reflect при первом прогоне — самая дорогая операция в библиотеке; второй прогон той же структуры заметно быстрее, потому что план уже в кэше. Именно поэтому в бенчмарках валидатора кэш прогревают заранее: сравнивать холодный первый вызов с горячими бессмысленно. Строку вывода вы получите только на локальной машине: песочница сайта валидатор не исполняет, поэтому примеры живут в терминале, а в браузере мы тренируемся на интерактивах и программах на чистом Go. Если после запуска захочется посмотреть на границу больше — добавьте в форму третье сломанное поле и убедитесь, что число записей в выводе равно числу провалившихся полей, а не числу тегов: это наблюдение станет ключом к уроку про структуру ошибок.
Var: проверка одного значения#
Структура есть не всегда: проверяемый возраст пришёл аргументом функции, значение — из query-параметра, элемент — из цикла по срезу. Для таких случаев у валидатора есть Var: одно значение и строка правил. Грамматика та же, что в тегах: required, min, max, email, gte — весь словарь доступен и поодиночке. Var хорош в утилитах и конвертерах, где хочется сохранить единый словарь правил с остальным проектом, а не плодить локальные if-ы: CLI-утилита и миграция проверяют аргументы тем же словарём, что и структуры основного сервиса, и правило не расползается на две реализации. В тестах Var тоже удобен: проверка граничного значения не требует заводить обёрточную структуру ради одного числа. Для сравнения двух значений существует VarWithValue — например, пароль и его подтверждение; в структурах та же задача решается тегом eqfield, о котором поговорим в уроке про условные теги. Таблица ниже — шпаргалка по методам проверки, которые пригодятся в первые недели; полный список богаче, но эти пять закрывают большую часть повседневных задач. Отдельный плюс Var — проверка на лету в тестах и утилитах командной строки: не нужно городить структуру ради одного значения, а словарь правил остаётся единым с остальным проектом. Помните только, что Var возвращает тот же контракт ошибок, что и Struct: не-структуры тут по определению не бывает, поэтому отдельная ветка с InvalidValidationError не нужна.
Var в функции-конвертере
GOfunc parseAge(raw string) (int, error) {
age, err := strconv.Atoi(raw)
if err != nil {
return 0, fmt.Errorf("возраст: %w", err)
}
if err := validate.Var(age, "gte=0,lte=130"); err != nil {
return 0, errors.New("возраст должен быть от 0 до 130")
}
return age, nil
}Число из строки проверяется одним вызовом Var с той же грамматикой, что и теги: gte=0,lte=130 сравнивает значение числа, а не длину. Ошибка Var — тот же ValidationErrors с одной записью, но для одиночного значения обычно хватает общего текста, как здесь. Переменная validate — всё тот же единственный экземпляр на приложение: Var пользуется тем же кэшем правил.
Методы *Validate, которые пригодятся сразу#
| Метод | Что проверяет | Когда брать |
|---|---|---|
| Struct(s) | всю структуру по тегам полей | основной путь: формы, DTO, конфиги |
| Var(field, tag) | одно значение по строке правил | параметры функций, query-значения |
| VarWithValue(a, b, tag) | одно значение против другого | пароль и подтверждение, на лету |
| StructPartial(s, поля...) | только перечисленные поля | PATCH-обновления, пошаговые формы |
| StructExcept(s, поля...) | структуру без перечисленных полей | когда исключить проще, чем перечислить |
Четыре грабли на первой неделе#
- импорт без /v10 — компилятор не найдёт пакет: мажорная версия в пути обязательна
- импорт написан, а go get не сделан — go mod tidy спасёт, но лучше сразу получить зависимость
- validate.Struct(nil) — это InvalidValidationError, а не тишина: nil отсекайте до вызова
- New() на каждый вызов — кэш планов переваривается заново, CPU уходит в reflect
Классика новичка: validate := validator.New() внутри функции-обработчика запроса. Работает — и незаметно жжёт процессор: при каждом запросе reflect заново разбирает теги структур, а кэш планов живёт и умирает вместе с локальной переменной. Под нагрузкой растут P99 и потребление CPU, и профилировщик покажет reflect главным едоком. Лечение простое: пакетная переменная var validate = validator.New() или поле сервиса, созданное в конструкторе.
Проверка: nil на входе
Что вернёт validate.Struct(nil)?
Соберите первую программу
Восстановите импорт, конструктор и вызов проверки — три пропуска в коде и один в пути импорта.
Соберите путь первой программы
От команды в терминале до печати результата — расставьте строки по порядку.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Десятая ветка — актуальная: новые опции добавляются, старый код не ломается. Одна из недавних — WithRequiredStructEnabled: до v10.13 вложенная nil-структура молча пропускалась, опция делает её обязательной; пригодится в уроке про вложенные структуры. Девятая версия — легаси: путь без /v10, другой набор тегов и ноль развития; если видите её в туториале, уроку лет пять.
Карточки: первый словарь
В третьем уроке — теги required, min и max: на одной форме разберём обязательность, длины и диапазоны, включая ловушку с нулевыми значениями. Пятый урок разбирает ValidationErrors по методам — Field, Tag, Param — это материал для сообщений на русском из шестого. Правило одного экземпляра, которое мы сегодня сформулировали, доживёт до своего отдельного урока с разговором о кэше структур и ценах. Если ваш проект на Gin, параллельно читайте урок про валидацию в Gin: там тот же движок уже включён и ждёт тегов binding.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Установка — go mod init и go get github.com/go-playground/validator/v10: мажорная версия в пути импорта обязательна.
- Пакет по пути с /v10 называется validator: конструктор New, проверки — Struct и Var.
- Struct возвращает nil при успехе, ValidationErrors при провале правил и InvalidValidationError на не-структуру.
- Экземпляр валидатора один на приложение: кэш планов дорог, New в хендлере сжигает CPU на reflect.
- Дефолтный вывод ошибки — английская простыня Key: '...': её приручают каталогом сообщений и конвертом 400.
- Неизвестный тег — паника на первом прогоне структуры: тест с валидным значением обязателен.
Домашнее задание
Соберите программу урока локально и расширьте SignupForm двумя полями: Nick string с required,min=3,max=32 и Site string с omitempty,url. Прогоните пять случаев: валидная форма, пустой nick, короткий nick, сайт без схемы и validate.Struct(nil). Запишите, на каких тегах споткнулся каждый случай, — этот список пригодится в уроке про ValidationErrors.
Личные заметкиПусто
Вопросы и ответы по уроку
Какая версия validator сейчас актуальна?
Десятая: путь импорта github.com/go-playground/validator/v10. Девятая — легаси без развития; если путь в туториале без /v10, материалу несколько лет. Минорные обновления внутри десятой ветки API не ломают.
Почему в пути импорта нужен /v10?
Требование Go-модулей: со второй мажорной версии номер входит в путь пакета. Без суффикса Go ищет девятую версию и падает с ошибкой, что модуль не найден.
Можно ли создавать валидатор на каждый запрос?
Технически да, практически нет: конструктор сам по себе дёшев, но кэш планов создаётся заново и каждый вызов проходит через reflect. Один экземпляр на приложение — стандартная практика экосистемы: пакетная переменная в пакете internal/validate или поле сервиса, передаваемое зависимостью, — тогда кэш живёт всё время работы приложения, а в тестах валидатор подменяется как любое другое поле.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Asynq в Go: очередь фоновых задач на Redis
Курс asynq для Go: клиенты и воркеры, отложенные и периодические задачи, ретраи, очереди с приоритетами, агрегация, Inspector и asynqmon. 25 уроков.
- Строить клиент и воркер asynq и подключать Redis
- Отправлять отложенные, периодические и уникальные задачи
- Настраивать ретраи, таймауты и приоритеты очередей
- Мониторить задачи через Inspector и asynqmon