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