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

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

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

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

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

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

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

Коротко

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

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

  • Обработчики и JSON в Go — как декодировать тело запроса до валидации.
  • Ошибки и HTTP в Go — как превратить ошибку валидации в правильный HTTP-ответ.
  • Middleware в Go — как вынести повторяющиеся проверки в цепочку обработчиков.