Валидация в Go — на границе и явная. Нет аннотаций-магии, которые фреймворк сам подхватит; ты либо запускаешь валидатор по тегам структуры, либо проверяешь поля вручную. И то, и другое — осознанный вызов в обработчике, а не скрытый шаг.
Зачем граница
После декодирования JSON у тебя есть структура, но нет гарантий: имя могло прийти пустым, цена — отрицательной. Валидация — это граница доверия: до неё данные «какие пришли», после — проверенные. В UCP эта граница чёткая: формат проверяется тут, на входе, а бизнес-правила — глубже, в Handler-е и домене.
Валидатор по тегам
Самый частый инструмент — 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)
}
validate.Struct вернёт ошибку, если поля не прошли правила. Валидатор обычно создают один раз (он потокобезопасен) и переиспользуют. В обработчике это идёт сразу после декодирования:
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
if err := req.Validate(); err != nil {
writeError(w, http.StatusBadRequest, err)
return
}
Ручная валидация
Когда правило тегами не выразить — зависит от нескольких полей или требует своей логики — пишут проверку руками. В Go это нормально и читаемо.
func (req CreateDiscountRequest) Validate() error {
if req.DiscountPrice >= req.Price {
return fmt.Errorf("discount_price must be lower than price")
}
return nil
}
Часто оба подхода сочетают: теги для простых правил, ручная проверка для связей между полями. Главное — собрать валидацию в одном методе Validate() структуры, чтобы обработчик вызывал её одной строкой.
Граница: формат и правило
Та же дисциплина, что во всех биндингах: валидатор проверяет формат — длина, диапазон, обязательность, согласованность полей запроса. Бизнес-правило — занято ли имя, допустима ли операция в текущем состоянии — это Handler и домен, не структура запроса. Тянуть в Validate() обращение к базе — значит размывать границу.
Различие простое: формат можно проверить, глядя только на сам запрос; правило требует знания о состоянии системы. Первое — здесь, на краю; второе — глубже. Это аналог Bean Validation против доменных правил в Spring-биндинге, только без аннотаций-магии: в Go проверка — явный вызов, который видно в обработчике. Ошибки валидации переводятся в HTTP единообразно — об этом следующая статья. Чёткая граница формата — то, что позволяет продукт-инженеру доверять данным внутри сервиса.