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

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

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

Урок 1 из 25 ~16 мин +20 XP

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

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

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

Начнём с конкретного 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) — она считает байты, а для кириллицы это не то, что ожидает пользователь. Теги из следующего блока заменяют всё это одной строкой на поле.

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

Раз проверок много, они скучные и одинаковые — их выносят в библиотеку. Де-факто стандарт в Go — go-playground/validator: проект живёт в организации go-playground на GitHub, десятая мажорная версия (ветка v10) вышла в 2019 году и развивается до сих пор. Библиотека не привязана к HTTP: это движок проверки структур по тегам, который фреймворки зовут под капотом. В Gin он стоит за тегами binding — мы разбирали его в уроке про валидацию в Gin; 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 на поле?

Валидация за пределами 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 валидатора.

type RegisterForm struct { Email string `validate:",email"` Password string `validate:"required,=8"` Age int `validate:"gte=0,"` } validate := validator.() err := validate.(&form)

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

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

Строки

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

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

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

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

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

Термин

go-playground/validator

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

Движок валидации структур по тегам; актуальная ветка v10 (с 2019 года). Стоит под Gin, используется в Echo и Fiber, работает и вне HTTP — конфиги, очереди, CLI.

1 / 6

Во втором уроке — установка и первая валидация: 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.

Личные заметкиПусто
Хранятся только в вашем браузере (Validator · Знакомство: зачем валидация всегда нужна)

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

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

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

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

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

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

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

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

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

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

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

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

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

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