Содержание урока · 10 разделов
- Зачем валидация golang: дырявое API и мусор в базе
- Что прилетает на настоящий эндпоинт
- Цена ручных if: три способа потерять контроль
- go-playground/validator: стандарт экосистемы Go
- Валидация за пределами HTTP: конфиги, очереди, DTO
- Способы проверки данных в 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. Сообщения об ошибках в каждой ветке свои: где-то по-русски, где-то по-английски, где-то вовсе без текста. Ручной подход не запрещён — он просто не масштабируется и не переживает смену трёх разработчиков, потому что живёт не в одном месте, а во всей кодовой базе сразу.
Ручная валидация: так выглядит попытка
GOfunc 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; Echo и Fiber в своих интеграциях используют тот же движок. За пределами HTTP то же самое API проверяет конфиги, DTO на границе слоёв и данные фоновых задач. Отсюда практический вывод: учить валидатор выгодно один раз — навык переносится между фреймворками и проектами, а синтаксис тегов встречается в половине открытых Go-репозиториев.
Превью: те же проверки — четырьмя тегами
GOtype 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 на поле?
Валидация за пределами HTTP: конфиги, очереди, DTO#
Соблазн считать валидацию темой только веб-фреймворков стоит дороже, чем кажется. Конфиг с портом 0 или таймаутом −1 соберётся успешно и упадёт в рантайме, далеко от места настоящей ошибки — опечатка в YAML обнаруживается по трейсу в проде, а не по красной строке в редакторе. Фоновая задача получит сломанный элемент очереди и умрёт ночью, когда дежурный спит. DTO на границе модуля пропустит мусор внутрь домена, и через год в команде уже никто не сможет сказать, где кончаются гарантии целостности. Отдельная история — ответы внешних API: контракт с партнёром кажется договором, но партнёры обновляют форматы и шлют null там, где вчера был массив; проверка ответа на границе превращает чужой релиз в вежливую ошибку в логах вместо падения вашего сервиса. Реимпорт старого дампа с пропущенными полями — ещё один классический источник дубликатов и отрицательных балансов, и он приходит мимо HTTP вовсе. Практическое правило: валидируйте на границах — входящие HTTP-запросы, загруженные конфиги, задачи из очередей, ответы внешних систем; внутри проверенных границ можно доверять типам и не проверять одно и то же по три раза. Дешёвая привычка, которая окупается: у каждой точки входа данных спрашивать «кто проверил это значение?» — и если ответ «никто», добавлять проверку до бизнес-логики, а не после инцидента. Как расставить границы по слоям и почему доменные модели плохо проверять тегами — отдельные уроки курса: про DTO-границы API и про валидацию конфигов с Viper.
Способы проверки данных в Go#
| Подход | Где живёт правило | Цена при росте проекта |
|---|---|---|
| Ручные if в хендлере | в коде обработчика | дублируется, расползается, забывается |
| Своя функция на каждое поле | в утилитных пакетах | лучше if-ов, но API проверки у каждого своё |
| go-playground/validator | тег у поля структуры | одна строка на правило, один вызов на структуру |
| JSON Schema и генераторы | внешняя спека | мощно, но избыточно без нескольких языков-потребителей |
Мнение, которое сэкономит вам рефакторинг: теги валидатора — не место для бизнес-правил. Правило «баланс не уходит в минус» можно выразить сравнением полей, но изменится оно, когда появятся кредитные лимиты, — а тег, спрятанный в структуре, окажется в худшем месте для изменений. Граница, которой пользуюсь я: формат, длины, диапазоны, перечни — тегам; правила, которые звучат как требования продукта, — отдельному слою бизнес-проверок. Когда через полгода будете искать, где править лимит, скажете себе спасибо.
Что вернёт проверка
Прочитайте код и выберите, что он выведет:
type Input struct {
Age int `validate:"gte=0,lte=130"`
Mail string `validate:"email"`
}
in := Input{Age: -3, Mail: "не-почта"}
err := validate.Struct(in)
// что лежит в err?Проверка: где живёт движок
Какие фреймворки используют go-playground/validator?
Восстановите превью-структуру
Заполните пропуски: обязательность и формат почты, минимальная длина пароля, верхняя граница возраста и два вызова API валидатора.
Соберите путь запроса с валидацией
POST /register с плохим email и отрицательным возрастом. Разложите события от curl до ответа клиенту.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Границы тегов: что валидатор не решит#
Честная оговорка перед практикой: теги — не серебряная пуля. Правила на комбинацию полей («пароль и его подтверждение совпадают») в тегах одного поля не живут — для этого есть StructLevel-проверки, кросс-польные сравнения и свои функции на RegisterValidation, под которые в курсе отведены отдельные уроки. Бизнес-правила вроде лимитов и балансов — не теговая территория, о чём говорили выше. Нормализация — обрезка пробелов и нижний регистр почты — отдельный шаг перед валидацией, иначе « a@b.ru » и «a@b.ru» станут двумя разными судьбами одного адреса. И последнее: теги — строки, компилятор их не проверяет; опечатка в имени тега выстрелит паникой Undefined validation function на первом же прогоне структуры. Отсюда рабочее правило курса: каждую структуру с тегами покрываем тестом с заведомо валидным значением — он ловит битые теги до продакшена.
Карточки: словарь валидации
Код курса библиотечный: валидатор не входит в стандартную поставку Go, и браузерная песочница сайта его не исполняет. Интерактивы — тесты, пропуски, сборка по строкам — проверяют понимание без запуска, а примеры собирайте локально: понадобится Go и go get, пройдём установку во втором уроке. Если структуры, ошибки и указатели пока на уровне «читал, но не писал», начните с базового курса — без них валидатор превращается в копипасту без понимания.
Во втором уроке — установка и первая валидация: go get, validator.New, первый вызов Struct и честный разбор nil против ошибок. Третий урок посвящён тегам required, min и max на живой форме, четвёртый — форматам вроде email, url и uuid. Структуру ValidationErrors разбираем в пятом уроке — к этому моменту вы увидите её на собственном коде. Интеграции с HTTP-фреймворками — в уроках про Gin и Echo ближе к середине курса, а антипаттерны ручных if, которые мы сегодня заклали основой, разберём отдельно, чтобы не воспроизводить в новом виде.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Валидация — это граница доверия: данные из сети, конфигов и очередей проверяются до бизнес-логики.
- Пропущенная проверка не падает с ошибкой — она тихо пишет мусор в базу и ломает отчёты и кассу.
- Ручные if дублируются, расползаются и забываются; на росте проекта код проверок обгоняет логику.
- go-playground/validator v10 — стандарт экосистемы: движок под Gin, Echo и Fiber, работает и вне HTTP.
- Правила живут в тегах у поля, один вызов Struct() проверяет всю структуру и собирает список провалов.
- Теги — для формата, длины и диапазонов; бизнес-правила — отдельный слой, иначе рефакторинг захлебнётся.
- Теги не проверяются компилятором: битое имя тега — паника в рантайме, лечится тестом с валидным значением.
Домашнее задание
Возьмите любой хендлер своего проекта (или напишите список на бумаге) и выпишите каждое правило ручной проверки отдельной строкой: поле, условие, текст ошибки. Пометьте, какие правила дублируются в других местах и какие для одного смысла написаны по-разному. Этот список превратится в теги во втором уроке, когда мы подключим validator, — заодно вы увидите, сколько строк кода съедает ручной подход. Заодно пометьте, какие проверки могли бы жить на фронте, а какие обязаны остаться на сервере — это различие нам ещё понадобится при разборе DTO.
Личные заметкиПусто
Вопросы и ответы по уроку
Обязательна ли серверная валидация, если фронтенд уже проверяет поля?
Да. Фронтенд — удобство, а не граница: curl, старые версии мобильного приложения и другие клиенты минуют его проверки. Сервер проверяет всё, что приходит из сети, потому что после декодирования не знает, откуда данные; у чужого клиента нет вашей формы со всеми её проверками.
Чем validator отличается от проверок самого фреймворка?
Фреймворки не пишут свои валидаторы: Gin, Echo и Fiber используют тот же go-playground/validator под капотом и дают над ним обёртку. Синтаксис тегов учат один раз — работает везде.
Можно ли выразить тегами бизнес-правила?
Формально можно — диапазоны и сравнение полей теги умеют. Практически лучше не надо: правила продукта меняются, а тег спрятан в структуре. Теги — для формата, длины, диапазона и перечня значений; остальное — свой слой проверок, который правится в одном месте и проходит код-ревью как обычный код.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Asynq в Go: очередь фоновых задач на Redis
Курс asynq для Go: клиенты и воркеры, отложенные и периодические задачи, ретраи, очереди с приоритетами, агрегация, Inspector и asynqmon. 25 уроков.
- Строить клиент и воркер asynq и подключать Redis
- Отправлять отложенные, периодические и уникальные задачи
- Настраивать ретраи, таймауты и приоритеты очередей
- Мониторить задачи через Inspector и asynqmon