Когда 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 — как вынести повторяющиеся проверки в цепочку обработчиков.