---
title: "Установка и первая валидация"
description: "go-playground validator установка на практике: go get v10, validator.New, первый вызов Struct и контракт nil против ValidationErrors с разбором каждой строки."
course: "Валидация в Go: go-playground/validator"
url: "https://golangmaster.ru/courses/validator/lessons/02-ustanovka-pervaya/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

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

*Курс «Валидация в Go: go-playground/validator», урок 2 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/validator/lessons/02-ustanovka-pervaya/*

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

Понадобится локальный 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](/courses/gin/lessons/09-validaciya/).

### Первая валидация: 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), если все поля прошли проверку?

- ✅ nil — дальше работает обычная логика хендлера
- Пустой ValidationErrors длиной 0
- true
- Скопированную структуру с отметками о пройденных тегах

Успех — это nil, и на нём строится весь контроль потока: if err != nil. Пустой срез ошибок валидатор не возвращает — нет провалов, нет значения: контракт специально совпадает с идиомой Go об ошибках, поэтому ничего запоминать не нужно.

### Что напечатает программа

```go
type SignupForm struct {
        Email string `validate:"required,email"`
}

form := SignupForm{Email: "не-почта"}
err := validate.Struct(&form)
fmt.Println(err)
// что выйдет на экран?
```

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

- email — имя провалившегося тега
- ✅ Key: 'SignupForm.Email' Error:Field validation for 'Email' failed on the 'email' tag
- nil — валидатор печатает предупреждения сам, не возвращая ошибку
- Паника Undefined validation function 'email'

Печать ValidationErrors вызывает метод Error(), который склеивает построчно записи вида Key: '...' Error:... Так выглядит дефолтное сообщение: подробное, английское и не для пользователей. Из его частей — имя поля, тег, параметр — в уроке про ValidationErrors мы соберём понятные тексты.

### Первый запуск: что вы увидите на экране

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

> **validator.New() внутри хендлера.** Классика новичка: validate := validator.New() внутри функции-обработчика запроса. Работает — и незаметно жжёт процессор: при каждом запросе reflect заново разбирает теги структур, а кэш планов живёт и умирает вместе с локальной переменной. Под нагрузкой растут P99 и потребление CPU, и профилировщик покажет reflect главным едоком. Лечение простое: пакетная переменная var validate = validator.New() или поле сервиса, созданное в конструкторе.

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

**Вопрос:** Что вернёт validate.Struct(nil)?

- nil — нечего проверять, всё прошло
- ✅ *validator.InvalidValidationError — передана не структура
- ValidationErrors с одной записью про пустую структуру
- Панику Undefined validation function

Struct принимает указатель на структуру; nil, число или срез — это *validator.InvalidValidationError, отдельный тип ошибки программиста. Его обрабатывают другой веткой errors.As и обычно завершают программу на старте — пользователю такое сообщение не показывают.

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

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

```go
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("всё чисто")
}
```

Подсказка: Мажорная версия — v10; имя пакета — validator; конструктор — New; проверка структуры — Struct.

Правильные ответы в пропусках: `v10`, `validator`, `New`, `Struct`

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

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

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

1. `go mod init demo`
2. `go get github.com/go-playground/validator/v10`
3. `type SignupForm struct { Email string `validate:"required,email"` }`
4. `validate := validator.New()`
5. `form := SignupForm{Email: "a@b.ru"}`
6. `err := validate.Struct(&form)`
7. `if err != nil { fmt.Println(err); return }`
8. `fmt.Println("всё чисто")`

> **Версии и полезная опция.** Десятая ветка — актуальная: новые опции добавляются, старый код не ломается. Одна из недавних — WithRequiredStructEnabled: до v10.13 вложенная nil-структура молча пропускалась, опция делает её обязательной; пригодится в уроке про вложенные структуры. Девятая версия — легаси: путь без /v10, другой набор тегов и ноль развития; если видите её в туториале, уроку лет пять.

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

- **validator.New()** — Конструктор *Validate. Один экземпляр на приложение: при первом прогоне структуры парсит теги в кэш; проверки потокобезопасны, регистрация тегов — нет.
- ***Validate** — Объект-валидатор с методами Struct, Var, VarWithValue, StructPartial и реестром правил. Создаётся один раз и делится всеми запросами.
- **validate.Struct(s)** — Проверка структуры по тегам: nil — всё чисто; ValidationErrors — список провалов; *InvalidValidationError — передали не структуру.
- **ValidationErrors** — Срез FieldError: поле, тег, параметр. Печатается строками Key: '...' Error:... — исходник для понятных сообщений.
- **InvalidValidationError** — Ошибка программиста: Struct(nil) или не-структура. В лог или panic на старте, пользователю не показывается.
- **validate.Var(v, tag)** — Проверка одиночного значения: validate.Var(age, "gte=0,lte=130"). Грамматика правил та же, что в тегах структур.

В третьем уроке — [теги required, min и max](/courses/validator/lessons/03-tegi-required-min-max/): на одной форме разберём обязательность, длины и диапазоны, включая ловушку с нулевыми значениями. [Пятый урок](/courses/validator/lessons/05-oshibki-validacii/) разбирает ValidationErrors по методам — Field, Tag, Param — это материал для сообщений на русском из шестого. Правило одного экземпляра, которое мы сегодня сформулировали, доживёт до своего [отдельного урока](/courses/validator/lessons/14-odin-ekzemplar/) с разговором о кэше структур и ценах. Если ваш проект на Gin, параллельно читайте урок про валидацию в Gin: там тот же движок уже включён и ждёт тегов binding.

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

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