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

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

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

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

Установка и первая валидация

Установка validator в Go-проект — это go get по пути с /v10 и один вызов validator.New(). В этом уроке соберём первую программу валидации: структура с тегами, вызов Struct() и разбор контракта nil против ValidationErrors — плюс методы Var и StructPartial для случаев, когда структуры нет под рукой. Каждую строку разберём по слогам; для запуска понадобится локальный Go.

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

Понадобится локальный 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 целиком

GO
package 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 в функции-конвертере

GO
func 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

Проверка: nil на входе

Что вернёт validate.Struct(nil)?

Соберите первую программу

Восстановите импорт, конструктор и вызов проверки — три пропуска в коде и один в пути импорта.

import ( "fmt" "github.com/go-playground//" ) func main() { validate := validator.() form := SignupForm{Email: "a@b.ru", Password: "длинный-пароль"} if err := validate.(&form); err != nil { fmt.Println("проверка не прошла:", err) return } fmt.Println("всё чисто") }

Соберите путь первой программы

От команды в терминале до печати результата — расставьте строки по порядку.

Строки

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

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

Карточки: первый словарь

Термин

validator.New()

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

Конструктор *Validate. Один экземпляр на приложение: при первом прогоне структуры парсит теги в кэш; проверки потокобезопасны, регистрация тегов — нет.

1 / 6

В третьем уроке — теги 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 · Установка и первая валидация)

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

Какая версия validator сейчас актуальна?

Десятая: путь импорта github.com/go-playground/validator/v10. Девятая — легаси без развития; если путь в туториале без /v10, материалу несколько лет. Минорные обновления внутри десятой ветки API не ломают.

Почему в пути импорта нужен /v10?

Требование Go-модулей: со второй мажорной версии номер входит в путь пакета. Без суффикса Go ищет девятую версию и падает с ошибкой, что модуль не найден.

Можно ли создавать валидатор на каждый запрос?

Технически да, практически нет: конструктор сам по себе дёшев, но кэш планов создаётся заново и каждый вызов проходит через reflect. Один экземпляр на приложение — стандартная практика экосистемы: пакетная переменная в пакете internal/validate или поле сервиса, передаваемое зависимостью, — тогда кэш живёт всё время работы приложения, а в тестах валидатор подменяется как любое другое поле.

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

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

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

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

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

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

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