---
title: "Знакомство: зачем валидация всегда нужна"
description: "Зачем валидация golang нужна каждому API: дырявые эндпоинты, мусор в базе, цена ручных if и знакомство с go-playground validator — первый урок курса."
course: "Валидация в Go: go-playground/validator"
url: "https://golangmaster.ru/courses/validator/lessons/01-znakomstvo-validator/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# Знакомство: зачем валидация всегда нужна

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

Зачем валидация golang-сервису отдельная библиотека, если хватит десятка if? Ответ приходит вместе с первым инцидентом: без проверок на границе API принимает возраст −5, email без собаки и скидку 150%, а мусор оседает в базе и всплывает месяцами. В первом уроке курса разберём, где дырявые приложения теряют деньги, во что выливается ручная проверка полей и почему теги у структур стали стандартом экосистемы Go.

Начнём с конкретного POST /register, который приходит в сервис без валидации: {"email":"привет","age":-5,"password":""}. Декодер JSON отработает честно: синтаксис корректен, типы совпали, структура заполнена. Дальше начинается интересное. Письмо с подтверждением не уйдёт, потому что адреса «привет» не существует. Возраст −5 уедет в аналитику и сломает сегментацию аудитории. Аккаунт с пустым паролем создастся и станет самым лёгким входом в систему. И самое коварное — ни один из этих сценариев не упадёт с ошибкой: сервис будет работать, причём работать правильно, если мерить его собственным кодом. Плохие данные приходят не только от злоумышленников: пользователь опечатался, старая версия мобильного приложения шлёт другой формат, фронтенд-баг обрезал поле, тестировщик постучался curl-ом напрямую. Данные портятся и без злого умысла: часовой пояс сдвинул дату, локаль подставила запятую вместо точки в дроби, копипаст принёс неразрывный пробел, который выглядит как обычный. Каждый из этих случаев проходит декодирование без единой ошибки — и становится вашей проблемой на следующей ступени. Валидация — это не война с хакерами, а граница доверия: данные из сети чужие по умолчанию, и проверять их нужно до бизнес-логики, а не внутри неё.

### Зачем валидация golang: дырявое API и мусор в базе

Три последствия пропущенной проверки выстраиваются в ступени, и каждая дороже предыдущей. Первая — пользовательский опыт: ошибка обнаруживается поздно, когда заказ уже не оформился, а перевод не ушёл; сообщение «что-то сломалось» вместо «телефон должен быть из 11 цифр» гарантирует тикет в поддержку. Вторая — данные: мусор в базе живёт годами, потому что однажды записанное значение читают отчёты, рассылки и новые фичи; чистка требует миграций, а миграции по грязным данным — отдельный болезненный жанр с ручными решениями по каждой строке. Третья — безопасность: неожиданные значения ломают допущения кода, от отрицательных сумм и скидок больше ста процентов до строк, которые не предназначались для SQL, логов или HTML. Хуже того, мусор редко остаётся на месте: значение из формы копируется в письмо, из письма в CRM, из CRM в отчёт, и к моменту обнаружения у него уже десяток копий, каждая со своей жизнью. Проверка на границе дёшева: один вызов валидатора против расследования инцидента, который начался со значения, никем не проверенного. Показательно и соотношение цен по времени: проверка пишется один раз при добавлении поля — тридцать секунд на строку тега; инцидент с грязными данными разбирается днями и требует согласования с бухгалтерией, поддержкой и аналитиками. Экономия на валидации — это не экономия, а кредит с очень неприятной процентной ставкой.

### Что прилетает на настоящий эндпоинт

- пустые строки и строки из одних пробелов — поле «заполнено», смысла в нём нет
- числа вне смысла задачи: возраст −5, скидка 150%, порт 99999
- email с двумя собаками, без домена или вовсе без @
- title на 50 000 символов — потому что textarea на фронте никто не ограничил
- null вместо пустого массива, строка вместо числа — клиенты бывают разными
- значения из перечня, которых в перечне нет: status: "почти оплачен"
- строки с HTML-разметкой и SQL-фрагментами — на случай, если их где-то склеят руками
- даты в трёх форматах сразу — от ISO до «завтра утром»

### Цена ручных if: три способа потерять контроль

Проверка руками выглядит честным решением: if form.Email == "" { return err }. На одном поле — да. На форме из десяти полей и трёх хендлерах это уже три десятка веток, и дальше начинается хорошо знакомый процесс гниения. Правила дублируются: email проверяют в регистрации, в подписке и в профиле — тремя похожими, но не одинаковыми кусками кода. Правила расползаются: каждый автор пишет проверку по-своему, где-то trim, где-то нет, и формы ведут себя по-разному в соседних разделах приложения. Правила забываются: добавили поле скидки — проверку написали, добавили поле валюты — забыли, и через полгода из отчётности вылезает валюта «руб.» вместо кода RUB. Сообщения об ошибках в каждой ветке свои: где-то по-русски, где-то по-английски, где-то вовсе без текста. Ручной подход не запрещён — он просто не масштабируется и не переживает смену трёх разработчиков, потому что живёт не в одном месте, а во всей кодовой базе сразу.

### Ручная валидация: так выглядит попытка

```go
func registerManual(form RegisterForm) error {
        if form.Email == "" {
                return errors.New("email обязателен")
        }
        if !strings.Contains(form.Email, "@") {
                return errors.New("email неправильный")
        }
        if len(form.Password) < 8 {
                return errors.New("пароль короткий")
        }
        if form.Age < 0 || form.Age > 130 {
                return errors.New("возраст странный")
        }
        return nil // и так на каждый хендлер, поле за полем
}
```

Четыре поля — пять веток и пять разных сообщений. Сюда не попали trim, проверка формата почты, длина имени и десяток правил настоящего проекта: код проверки быстро обгоняет бизнес-логику по объёму. Обратите внимание на len(form.Password) — она считает байты, а для кириллицы это не то, что ожидает пользователь. Теги из следующего блока заменяют всё это одной строкой на поле.

> **Что сломалось бы без проверки скидки.** Кейс из жизни e-commerce: поле Discount int уходит в сервис заказов без проверок. Фронт шлёт 10, всё хорошо, пока кто-то не дёргает API напрямую со значением 150 — скидка больше цены, итог отрицательный, заказ уходит в оплату с суммой «минус тысяча». Дальше расходятся касса, бухгалтерия и отчётность, а откатывать приходится не баг, а данные за неделю. Проверка gte=0,lte=100 на границе стоит одну строку тега.

### go-playground/validator: стандарт экосистемы Go

Раз проверок много, они скучные и одинаковые — их выносят в библиотеку. Де-факто стандарт в Go — go-playground/validator: проект живёт в организации go-playground на GitHub, десятая мажорная версия (ветка v10) вышла в 2019 году и развивается до сих пор. Библиотека не привязана к HTTP: это движок проверки структур по тегам, который фреймворки зовут под капотом. В Gin он стоит за тегами binding — мы разбирали его в уроке про [валидацию в Gin](/courses/gin/lessons/09-validaciya/); Echo и Fiber в своих интеграциях используют тот же движок. За пределами HTTP то же самое API проверяет конфиги, DTO на границе слоёв и данные фоновых задач. Отсюда практический вывод: учить валидатор выгодно один раз — навык переносится между фреймворками и проектами, а синтаксис тегов встречается в половине открытых Go-репозиториев.

### Превью: те же проверки — четырьмя тегами

```go
type RegisterForm struct {
        Email    string `validate:"required,email"`
        Password string `validate:"required,min=8"`
        Age      int    `validate:"gte=0,lte=130"`
        Discount int    `validate:"gte=0,lte=100"`
}

// Проверка всей формы — один вызов:
validate := validator.New()
if err := validate.Struct(RegisterForm{ /* поля */ }); err != nil {
        // err — validator.ValidationErrors: список полей и тегов,
        // на которых проверка провалилась (разбор — в уроке 05)
}
```

Правила переехали из веток if в теги рядом с полем: обязательность, формат почты, минимальная длина пароля и диапазоны — по строке на правило. Вызов validate.Struct обходит все поля через reflect и собирает провалы в один список ошибок; структуру ошибки и человекочитаемые сообщения на русском разбираем в уроках 05 и 06. Установка библиотеки — первый шаг следующего урока.

### Проверка: что значит required

**Вопрос:** Что в validator проверяет тег required на поле?

- Строка непустая после обрезки пробелов
- ✅ Значение поля — не нулевое значение своего типа: не "", не 0, не false, не nil
- Поле физически присутствует в JSON-теле запроса
- Поле заполнено значением, которое осмысленно для бизнес-логики

required — это проверка на нулевое значение типа: пустая строка, ноль, false, nil-указатель, пустой срез или карта. Проверить «присутствие в JSON» тег не может: после декодирования отсутствующее поле и поле с нулевым значением выглядят одинаково — для таких случаев придумали указатели, разберём их в уроке про теги required и min/max.

### Валидация за пределами HTTP: конфиги, очереди, DTO

Соблазн считать валидацию темой только веб-фреймворков стоит дороже, чем кажется. Конфиг с портом 0 или таймаутом −1 соберётся успешно и упадёт в рантайме, далеко от места настоящей ошибки — опечатка в YAML обнаруживается по трейсу в проде, а не по красной строке в редакторе. Фоновая задача получит сломанный элемент очереди и умрёт ночью, когда дежурный спит. DTO на границе модуля пропустит мусор внутрь домена, и через год в команде уже никто не сможет сказать, где кончаются гарантии целостности. Отдельная история — ответы внешних API: контракт с партнёром кажется договором, но партнёры обновляют форматы и шлют null там, где вчера был массив; проверка ответа на границе превращает чужой релиз в вежливую ошибку в логах вместо падения вашего сервиса. Реимпорт старого дампа с пропущенными полями — ещё один классический источник дубликатов и отрицательных балансов, и он приходит мимо HTTP вовсе. Практическое правило: валидируйте на границах — входящие HTTP-запросы, загруженные конфиги, задачи из очередей, ответы внешних систем; внутри проверенных границ можно доверять типам и не проверять одно и то же по три раза. Дешёвая привычка, которая окупается: у каждой точки входа данных спрашивать «кто проверил это значение?» — и если ответ «никто», добавлять проверку до бизнес-логики, а не после инцидента. Как расставить границы по слоям и почему доменные модели плохо проверять тегами — отдельные уроки курса: про DTO-границы API и про валидацию конфигов с Viper.

### Способы проверки данных в Go

| Подход | Где живёт правило | Цена при росте проекта |
| --- | --- | --- |
| Ручные if в хендлере | в коде обработчика | дублируется, расползается, забывается |
| Своя функция на каждое поле | в утилитных пакетах | лучше if-ов, но API проверки у каждого своё |
| go-playground/validator | тег у поля структуры | одна строка на правило, один вызов на структуру |
| JSON Schema и генераторы | внешняя спека | мощно, но избыточно без нескольких языков-потребителей |

> **Спорное: теги — не место для бизнес-правил.** Мнение, которое сэкономит вам рефакторинг: теги валидатора — не место для бизнес-правил. Правило «баланс не уходит в минус» можно выразить сравнением полей, но изменится оно, когда появятся кредитные лимиты, — а тег, спрятанный в структуре, окажется в худшем месте для изменений. Граница, которой пользуюсь я: формат, длины, диапазоны, перечни — тегам; правила, которые звучат как требования продукта, — отдельному слою бизнес-проверок. Когда через полгода будете искать, где править лимит, скажете себе спасибо.

### Что вернёт проверка

```go
type Input struct {
        Age  int    `validate:"gte=0,lte=130"`
        Mail string `validate:"email"`
}

in := Input{Age: -3, Mail: "не-почта"}
err := validate.Struct(in)
// что лежит в err?
```

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

- nil — валидатор проверяет только required, а его в тегах нет
- ✅ ValidationErrors с двумя записями: Age по gte=0 и Mail по email
- ValidationErrors с одной записью — валидатор останавливается на первом поле
- Паника: значения не похожи на настоящие данные

Validate проверяет все поля и собирает все провалы: Age не прошёл gte=0, Mail не прошёл email — две записи FieldError в ValidationErrors. Отсутствие required ни при чём: gte и email — самостоятельные проверки, ноль и пустая строка их не проходят. Останавливаться на первом поле валидатор не делает — клиент получает весь список проблем сразу.

### Проверка: где живёт движок

**Вопрос:** Какие фреймворки используют go-playground/validator?

- Только Gin — это его встроенный движок
- Gin и Echo, но не Fiber
- ✅ Gin, Echo и Fiber — и вообще он не привязан к HTTP
- Никакие: каждый фреймворк пишет валидацию сам

Gin держит validator v10 за интерфейсом StructValidator и переключает словарь тегов на binding; Echo и Fiber используют тот же движок в своих механизмах привязки. А сам validator — библиотека без зависимости от HTTP: конфиги, очереди и CLI пользуются им напрямую.

### Восстановите превью-структуру

Заполните пропуски: обязательность и формат почты, минимальная длина пароля, верхняя граница возраста и два вызова API валидатора.

```go
type RegisterForm struct {
        Email    string `validate:"___,email"`
        Password string `validate:"required,___=8"`
        Age      int    `validate:"gte=0,___"`
}

validate := validator.___()
err := validate.___(&form)
```

Подсказка: Обязательность — required; минимальная длина — min=8; верхняя граница — lte=130; конструктор — New; проверка структуры — Struct.

Правильные ответы в пропусках: `required`, `min`, `lte=130`, `New`, `Struct`

### Соберите путь запроса с валидацией

POST /register с плохим email и отрицательным возрастом. Разложите события от curl до ответа клиенту.

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

1. `curl отправляет POST /register: {"email":"привет","age":-5}`
2. `Хендлер декодирует тело запроса в структуру RegisterForm`
3. `validate.Struct(&form): движок обходит поля по тегам`
4. `Email проваливает тег email, Age проваливает gte=0`
5. `Возвращается validator.ValidationErrors с двумя FieldError`
6. `Хендлер собирает человекочитаемые сообщения по каждому полю`
7. `Клиент получает 400 и список проблем вместо тишины`
8. `В базу мусор не попадает — граница сработала`

### Границы тегов: что валидатор не решит

Честная оговорка перед практикой: теги — не серебряная пуля. Правила на комбинацию полей («пароль и его подтверждение совпадают») в тегах одного поля не живут — для этого есть StructLevel-проверки, кросс-польные сравнения и свои функции на RegisterValidation, под которые в курсе отведены отдельные уроки. Бизнес-правила вроде лимитов и балансов — не теговая территория, о чём говорили выше. Нормализация — обрезка пробелов и нижний регистр почты — отдельный шаг перед валидацией, иначе « a@b.ru » и «a@b.ru» станут двумя разными судьбами одного адреса. И последнее: теги — строки, компилятор их не проверяет; опечатка в имени тега выстрелит паникой Undefined validation function на первом же прогоне структуры. Отсюда рабочее правило курса: каждую структуру с тегами покрываем тестом с заведомо валидным значением — он ловит битые теги до продакшена.

### Карточки: словарь валидации

- **go-playground/validator** — Движок валидации структур по тегам; актуальная ветка v10 (с 2019 года). Стоит под Gin, используется в Echo и Fiber, работает и вне HTTP — конфиги, очереди, CLI.
- **тег validate** — Строка правил у поля: validate:"required,email". Правила разделяются запятыми, параметр идёт через =, исполняются слева направо до первого провала.
- **required** — Проверка не-нулевого значения типа: не "", не 0, не false, не nil, не пустой срез или карта. Про «присутствие в JSON» тег не знает.
- **omitempty** — Если поле нулевое — остальные правила тега пропускаются. Компаньон опциональных полей: omitempty,min=3 — пустое можно, непустое от трёх символов.
- **ValidationErrors** — Срез FieldError, который возвращается при провале validate.Struct: поле, тег, параметр — материал для понятных сообщений об ошибках.
- **граница доверия** — Данные из сети, конфигов и очередей проверяются до бизнес-логики; внутри проверенных границ доверяем типам и не дублируем проверки.

> **Как проходить курс.** Код курса библиотечный: валидатор не входит в стандартную поставку Go, и браузерная песочница сайта его не исполняет. Интерактивы — тесты, пропуски, сборка по строкам — проверяют понимание без запуска, а примеры собирайте локально: понадобится Go и go get, пройдём установку во втором уроке. Если структуры, ошибки и указатели пока на уровне «читал, но не писал», начните с [базового курса](/courses/go-basics/) — без них валидатор превращается в копипасту без понимания.

Во втором уроке — [установка и первая валидация](/courses/validator/lessons/02-ustanovka-pervaya/): go get, validator.New, первый вызов Struct и честный разбор nil против ошибок. Третий урок посвящён тегам required, min и max на живой форме, четвёртый — форматам вроде email, url и uuid. Структуру ValidationErrors разбираем в [пятом уроке](/courses/validator/lessons/05-oshibki-validacii/) — к этому моменту вы увидите её на собственном коде. Интеграции с HTTP-фреймворками — в уроках про Gin и Echo ближе к середине курса, а антипаттерны ручных if, которые мы сегодня заклали основой, разберём отдельно, чтобы не воспроизводить в новом виде.

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

- Валидация — это граница доверия: данные из сети, конфигов и очередей проверяются до бизнес-логики.
- Пропущенная проверка не падает с ошибкой — она тихо пишет мусор в базу и ломает отчёты и кассу.
- Ручные if дублируются, расползаются и забываются; на росте проекта код проверок обгоняет логику.
- go-playground/validator v10 — стандарт экосистемы: движок под Gin, Echo и Fiber, работает и вне HTTP.
- Правила живут в тегах у поля, один вызов Struct() проверяет всю структуру и собирает список провалов.
- Теги — для формата, длины и диапазонов; бизнес-правила — отдельный слой, иначе рефакторинг захлебнётся.
- Теги не проверяются компилятором: битое имя тега — паника в рантайме, лечится тестом с валидным значением.

## Домашнее задание

Возьмите любой хендлер своего проекта (или напишите список на бумаге) и выпишите каждое правило ручной проверки отдельной строкой: поле, условие, текст ошибки. Пометьте, какие правила дублируются в других местах и какие для одного смысла написаны по-разному. Этот список превратится в теги во втором уроке, когда мы подключим validator, — заодно вы увидите, сколько строк кода съедает ручной подход. Заодно пометьте, какие проверки могли бы жить на фронте, а какие обязаны остаться на сервере — это различие нам ещё понадобится при разборе DTO.

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

### Обязательна ли серверная валидация, если фронтенд уже проверяет поля?

Да. Фронтенд — удобство, а не граница: curl, старые версии мобильного приложения и другие клиенты минуют его проверки. Сервер проверяет всё, что приходит из сети, потому что после декодирования не знает, откуда данные; у чужого клиента нет вашей формы со всеми её проверками.

### Чем validator отличается от проверок самого фреймворка?

Фреймворки не пишут свои валидаторы: Gin, Echo и Fiber используют тот же go-playground/validator под капотом и дают над ним обёртку. Синтаксис тегов учат один раз — работает везде.

### Можно ли выразить тегами бизнес-правила?

Формально можно — диапазоны и сравнение полей теги умеют. Практически лучше не надо: правила продукта меняются, а тег спрятан в структуре. Теги — для формата, длины, диапазона и перечня значений; остальное — свой слой проверок, который правится в одном месте и проходит код-ревью как обычный код.
