← назад к разделу

GO КАК ФУНДАМЕНТ Валидация

Обязательно

Валидация в Go

Go

Проверка входных данных через теги и вручную, разница формата и бизнес-правила, полезные ошибки валидации.

1. Невалидный ввод нельзя пускать в домен

1 / 5

Пустые имена, отрицательные суммы и неверные типы ломают инварианты. Проверка стоит на границе обработчика.

  • Decode не проверяет смысл полей.
  • Ошибки ввода отличаются от системных сбоев.
  • Домен получает уже корректные данные.
inputvalidatedomainневерный ввод → 400

2. Теги описывают требования к полям

2 / 5

go-playground/validator проверяет структуру по тегам: required, email, min, max и другие.

  • Требования находятся рядом с типом входа.
  • Ошибка содержит имя поля и правило.
  • Кастомные правила добавляются отдельно.
CreateUserName requiredEmail emailAge gt=0Role oneofvalidator.StructОшибка = поле + правило

3. Ручная проверка подходит простым правилам

3 / 5

Если правило одно, внешняя библиотека не обязательна. Проверка через if читается явно и тестируется легко.

  • Сложные перекрёстные поля удобно проверять кодом.
  • Ошибку формируйте с конкретной причиной.
  • Не дублируйте одно правило в нескольких местах.
if end.Before(start)error: invalid rangeПростое правило - простой код

4. Формат и бизнес-правило различаются

4 / 5

Email по форме корректен, но может быть запрещён регистрацией. Положительная сумма может превышать лимит счёта.

  • Синтаксис проверяет валидатор.
  • Инварианты домена проверяет домен.
  • Ошибки этих уровней возвращают по-разному.
форматбизнес-правилоinputНельзя смешивать слои проверки

5. Ошибка помогает исправить ввод

5 / 5

Сообщение называет поле и причину: чего не хватает, какой формат ожидался или какое ограничение нарушено.

  • Required отличается от пустой строки и нулевого значения.
  • Множество ошибок возвращается списком.
  • Внутренние детали и SQL не показываются.
name: обязательное полеemail: ожидается user@example.comage: должно быть больше 0Поле + правило + ожидание

← назад к разделу

Когда HTTP-запрос приходит в приложение, данные в нём — произвольные. Имя пользователя может быть пустым, цена — отрицательной, дата — в неверном формате. Прежде чем что-то делать с этими данными, нужно убедиться: они соответствуют ожидаемому. Это и есть валидация.

В Go нет магии, которая проверяла бы данные автоматически. Каждая проверка — явный вызов в коде. Это делает поведение предсказуемым: видишь вызов — понимаешь, что тут происходит проверка.

Что происходит без валидации

После декодирования JSON структура заполнена — но её содержимое не гарантировано. Клиент мог не передать обязательное поле, передать отрицательное число там, где ожидается положительное, или строку длиннее допустимого.

Без проверки эти данные попадут в бизнес-логику — и там либо произойдёт panic, либо в базу запишется мусор, либо операция выполнится с неправильным результатом. Лучше отказать сразу, на входе, с понятным сообщением об ошибке.

Валидация по тегам: go-playground/validator

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

import "github.com/go-playground/validator/v10"

type CreateProductRequest struct {
    Name  string `json:"name"  validate:"required,max=200"`
    Price int    `json:"price" validate:"gt=0"`
}

var validate = validator.New()

func (req CreateProductRequest) Validate() error {
    return validate.Struct(req)
}

Здесь:

  • required — поле не должно быть пустым;
  • max=200 — строка не длиннее 200 символов;
  • gt=0 — число больше нуля.

validator.New() создают один раз: экземпляр потокобезопасен и переиспользуется на все запросы.

В обработчике проверка идёт сразу после декодирования:

func (h *ProductHandler) Create(w http.ResponseWriter, r *http.Request) {
    var req CreateProductRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, err)
        return
    }
    if err := req.Validate(); err != nil {
        writeError(w, http.StatusBadRequest, err)
        return
    }
    // данные проверены — можно работать дальше
}

Ручная проверка

Иногда тегом правило не выразить. Например: скидочная цена должна быть меньше обычной цены — это зависимость между двумя полями.

type CreateDiscountRequest struct {
    Price         int `json:"price"          validate:"gt=0"`
    DiscountPrice int `json:"discount_price" validate:"gt=0"`
}

func (req CreateDiscountRequest) Validate() error {
    if err := validate.Struct(req); err != nil {
        return err
    }
    if req.DiscountPrice >= req.Price {
        return fmt.Errorf("discount_price должна быть меньше price")
    }
    return nil
}

Сначала запускаем проверку по тегам, потом — дополнительное правило руками. Оба подхода прекрасно сочетаются в одном методе Validate().

Формат данных и бизнес-правило — разные вещи

Важно понимать разницу между двумя видами проверок:

Формат данных — можно проверить, глядя только на сам запрос. Пустое имя, отрицательная цена, некорректный email — это проблема самого запроса, никаких данных из базы не нужно. Такие проверки живут в Validate().

Бизнес-правило — требует знания о состоянии системы. «Имя продукта уже занято», «у пользователя нет прав на эту операцию», «товар нельзя удалить, пока есть активные заказы» — это нельзя решить, глядя только на запрос. Такие проверки живут глубже: в обработчике или в доменной логике, где есть доступ к базе данных.

Если тянуть запросы к базе внутрь Validate() — граница размывается. Потом сложно понять, где заканчивается проверка входных данных и начинается бизнес-логика.

Простой вопрос для ориентации: «могу ли я это проверить, не открывая базу?» Если да — это формат, и место проверки здесь. Если нет — это бизнес-правило, и место глубже.

Что делать с ошибками валидации

Когда Validate() вернул ошибку, её нужно превратить в HTTP-ответ со статусом 400. О том, как формировать единообразные ответы об ошибках и когда использовать 400, 422 и другие коды — в статье Ошибки и HTTP.

Дополнительно: при первом чтении можно пропустить

Глубже: required и нулевые значения, поля в ошибкерасширенное

У required есть ловушка, которая всплывает в первую неделю: тег проверяет, что значение не нулевое, а не что поле пришло в JSON. Price int validate:"required" отвергнет честную цену 0, а Enabled bool validate:"required" — любое false. Если ноль или false — допустимые значения, поле делают указателем (*int, *bool): отсутствие в JSON даст nil, и required проверит именно его. Для вложенных структур так же работает validator.New(validator.WithRequiredStructEnabled()) — без этой опции required на структуре проверяется по старым правилам.

Ошибка от validate.Struct — это validator.ValidationErrors, срез с одной записью на каждое сломанное правило. Клиенту отдают не её текст, а список полей: пройти по срезу через errors.As, взять e.Field() и e.Tag() и собрать {"field": "price", "rule": "gt"}. Из полезных тегов дальше понадобятся email, oneof=draft active, min/max для строк и срезов, dive — чтобы правило применилось к каждому элементу среза.

Коротко

  • Данные после декодирования не гарантированы: имя может быть пустым, число — отрицательным.
  • go-playground/validator позволяет задать правила тегами прямо на структуре и проверить всё одним вызовом validate.Struct(req).
  • Для правил между несколькими полями пишут ручную проверку — в Go это просто и читаемо.
  • Метод Validate() на структуре запроса объединяет оба подхода: обработчик вызывает его одной строкой.
  • Формат проверяется на входе, глядя только на запрос. Бизнес-правила — глубже, где есть доступ к состоянию системы.
  • required проверяет «не нулевое», а не «пришло в JSON»: для допустимых 0 и false поле делают указателем; ошибку ValidationErrors разбирают по полям, а не отдают текстом.

Что пощупать

Две границы проверки в одном сервисе практикума remodov/marketplace-system-go: формат запроса ловит обработчик и отвечает 400 с именем поля в errors, а смысл (цена больше нуля, скидка не глубже половины) проверяет сам тип Product.

Код: handler.go, product.go.

Сделаем сами

Ветка step-03-commands-and-errors.

Что почитать дальше