Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 12 из 25 ~18 мин +20 XP

Валидация в Gin: binding

Gin binding validation спрятан в один вызов: ShouldBindJSON декодирует тело и сам запускает движок validator v10 по тегам binding, вам остаётся разобрать ошибку. В этом уроке открываем капот — binding.Validator, SetTagName и дверь Engine() — прогоняем параметры маршрута через ShouldBindUri и собираем конверт 400, в котором каждая запись ValidationErrors превращена в понятное сообщение по полю.

Содержание урока · 10 разделов

Gin — самый распространённый способ встретить validator в карьере: большая часть проектов на gin пользуется им, не устанавливая библиотеку руками. Теги binding мы видели в уроках курса Gin — пришло время совместить их со всем, что вы умеете из первых одиннадцати уроков: с dive и вложенными структурами, условными тегами, своими функциями и StructLevel. Механика одна и та же — тот же движок v10, те же ValidationErrors и FieldError, — различие в обвязке: gin держит валидатор внутри и переключает его словарь с validate на binding. Отсюда два практических навыка урока: писать хендлер, который за один вызов привязывает и проверяет тело, и превращать сырую ошибку валидации в ответ, который фронтенд сможет подсветить по полям. Побочно наведём порядок в семье ShouldBind-методов: тело, query, заголовки и параметры пути проверяются одинаково, но разными точками входа.

gin binding validation: что внутри ShouldBindJSON#

Вызов c.ShouldBindJSON(&dto) делает две вещи подряд: декодирует JSON-тело в структуру и прогоняет её через валидатор. Вторая половина — не магия gin, а наш validator v10: пакетная переменная binding.Validator указывает на реализацию интерфейса StructValidator из двух методов — ValidateStruct(obj any) error и Engine() any. Дефолтная реализация держит *validator.Validate, который создаётся лениво под sync.Once, и в тот же момент вызывает SetTagName("binding") — вот почему правила в gin-проектах пишутся в теге binding, а не validate: словарь движка переключён, и теги validate он молча не читает. Gin validator кэширует разбор структур так же, как «голый» валидатор из второго урока, — один экземпляр на всё приложение gin создаёт и греет сам. Errors по-прежнему идут типом validator.ValidationErrors, и доставать их положено errors.As с указателем на срез — ровно та дисциплина пятого урока. Два отличия, о которых стоит помнить заранее: стат ShouldBindJSON за вас никто не ставит — ошибку в 400 превращает ваш хендлер, и массив верхнего уровня gin валидирует собственным циклом с типом binding.SliceValidationError — про него есть подробный разбор в уроке про валидацию курса Gin. Пропускная способность у ValidateStruct широкая, но выборочная: nil вернёт nil, указатель на структуру уедет в движок, срез или массив верхнего уровня — в цикл по элементам, а всё остальное — карта, строка, число — пройдёт молча без ошибки: движок проверяет только структуры. Отсюда правило DTO-дизайна: на вход принимайте указатель на структуру — единственный вид, для которого binding вообще что-то проверяет.

Хендлер регистрации: bind, разбор, конверт 400

GO
type SignupDTO struct {
        Email    string `json:"email" binding:"required,email"`
        Password string `json:"password" binding:"required,min=8"`
        Age      int    `json:"age" binding:"omitempty,gte=0,lte=130"`
}

func signup(c *gin.Context) {
        var in SignupDTO
        if err := c.ShouldBindJSON(&in); err != nil {
                var verrs validator.ValidationErrors
                if errors.As(err, &verrs) {
                        c.JSON(http.StatusBadRequest, gin.H{
                                "errors": respondFields(verrs), // карта поле → текст
                        })
                        return
                }
                c.JSON(http.StatusBadRequest, gin.H{"error": "битое тело запроса"})
                return
        }
        // in проверен: дальше бизнес-слой
        c.JSON(http.StatusCreated, gin.H{"status": "ok"})
}

ShouldBindJSON вернул err — статус ставим сами: это честная точка решения, а не скрытый 400. Первая ветка errors.As ловит ValidationErrors и отдаёт карту по полям, собранную из Field(), Tag() и Param() каталогом из шестого урока. Всё остальное — битый JSON, не-JSON, переполнение типов: общая фраза без деталей, потому что данных для подсветки нет.

Проверка: почему тег binding

В gin-проекте правила пишут в binding:"required", а не validate:"required". Почему?

ShouldBind против ShouldBindJSON: кто что читает#

Семейство ShouldBind устроено как розетки одного валидатора: сначала раскладка, затем проверка. ShouldBind выбирает источник сам: у GET — query-параметры, у остальных методов — JSON-тело, поэтому DELETE при таком подходе обязан прислать тело, как POST; удобно для общих хендлеров, но в явных API точечные вызовы читаются честнее. ShouldBindJSON — только тело по Content-Type json. ShouldBindQuery разбирает query-строку по тегу form, ShouldBindHeader — заголовки по тегу header, ShouldBindUri — параметры маршрута по тегу uri. Рядом живут и служебные точки входа, о которых туториалы молчат: ShouldBindXML и ShouldBindYAML читают соответствующие форматы, а ShouldBindBodyForRepeatableRead буферизует тело и позволяет привязать его второй раз — например, когда после валидации нужно перечитать сырой JSON для лога или подписи. Все они заканчиваются одним и тем же: валидация по тегам binding тем самым движком, поэтому ошибки и обработка не различаются от ветки к ветке. Синтаксис правил — весь курс целиком: min, max, oneof, dive по срезам, условные required_if, ваши RegisterValidation и StructLevel — ничего переучивать не нужно. Единственный нюанс раскладки, который бьёт по валидации: конвертация строк в числа происходит при привязке, и «abc» для int-поля — ошибка декодера, а не провал тега; в ответе это тот же 400, но без записей ValidationErrors, и вашей ошибке-обёртке лучше об этом честно сказать клиенту.

Семейство ShouldBind: источник, тег, проверка#

МетодИсточник данныхТег раскладкиВалидация binding
ShouldBindпо методу: GET — query, остальные — телоform / jsonда
ShouldBindJSONтело запросаjsonда
ShouldBindQueryquery-строкаformда
ShouldBindHeaderHTTP-заголовкиheaderда
ShouldBindUriпараметры маршрута :iduriда

ShouldBindUri: параметры маршрута под правилами#

Параметры пути — самое незащищённое место типичного gin-проекта: c.Param("id") отдаёт строку, и до базы доезжает всё, что вписали в URL. ShouldBindUri закрывает дыру структурой: gin собирает c.Params в карту, поля заполняются по тегу uri, и дальше — стандартная валидация binding. Тип конвертируется сам: ID int64 снимет strconv, а для строковых идентификаторов пригодится uuid из четвёртого урока. required на uri-поле обычно бессмыслен: сегмент, не подходящий под :id, просто не сматчит маршрут, — зато oneof, min и uuid работают в полный рост и отсекают мусор до бизнес-логики. Ошибки — те же ValidationErrors, и обработка не отличается от тела запроса: та же ветка errors.As, тот же каталог сообщений. Учтите и обратную сторону: значения из пути приходят строками, конвертация в числа происходит при раскладке, и «abc» в int-поле вернёт ошибку конвертера — она приедет из ShouldBindUri как bind-ошибка, без записей ValidationErrors, и обработчик у неё общий с «битым телом». Полезная привычка — держать структурки параметров рядом с маршрутом и не тащить в них json-теги: источник данных у параметров один, и двойной словарь тегов только путает ревьюера.

Маршрут заказа: uuid и oneof на параметрах

GO
type OrderParams struct {
        ID     string `uri:"id" binding:"required,uuid"`
        Status string `uri:"status" binding:"required,oneof=new paid done"`
}

router.GET("/orders/:id/:status", func(c *gin.Context) {
        var p OrderParams
        if err := c.ShouldBindUri(&p); err != nil {
                var verrs validator.ValidationErrors
                if errors.As(err, &verrs) {
                        c.JSON(http.StatusBadRequest,
                                gin.H{"errors": respondFields(verrs)})
                        return
                }
                c.JSON(http.StatusBadRequest, gin.H{"error": "битые параметры"})
                return
        }
        c.String(http.StatusOK, "order %s is %s", p.ID, p.Status)
})

Имя в uri-теге обязано совпадать с сегментом маршрута посимвольно — опечатка оставит поле нулевым молча, и провалится уже required. oneof на статусе отсекает /orders/x/cancelled ещё до хендлера. Обработка ошибки идентична телу запроса: errors.As, respondFields, 400 — у клиента один формат ответа на все источники данных.

Что вернёт ShouldBindJSON

Прочитайте код и выберите, что он выведет:

type SignupDTO struct {
        Email    string `json:"email" binding:"required,email"`
        Password string `json:"password" binding:"required,min=8"`
}

// тело запроса:
// {"email": "ada@example.com", "password": ""}

err := c.ShouldBindJSON(&in)

Ошибки по-человечески: конверт для фронтенда#

respondFields — та самая функция из пятого и шестого уроков, и в gin ей живётся спокойнее всего: единственная точка оборачивания ValidationErrors в ответ, один формат на все ShouldBind-ветки. Помните про два имени поля: Field() возвращает имя структуры, фронтенд подсвечивает json-имя — маппинг из пятого урока нужен здесь нисколько не меньше. Для массивов верхнего уровня не забывайте вторую ветку: gin соберёт ошибки элементов в binding.SliceValidationError со строками вида [1]: ..., и errors.As на ValidationErrors мимо — элементу ответа лучше указать индекс виновника. Если ответ уезжает в JSON, не отдавайте err.Error() целиком: это склейка английских простыней, которую клиент не разберёт, а лог — наоборот, увидит её с благодарностью: одна строка с полным списком провалов делает запись о 400 самодостаточной. PATCH-обновлениям в gin тоже есть место: через Engine() доступен StructPartial с перечнем пришедших полей — проверяются только они, и клиент не обязан присылать форму целиком; вызов встаёт после декода и до записи в базу, ошибки — те же ValidationErrors. И не пытайтесь перехватывать валидацию раньше времени: даже если поле пришло, проверку остановит первый провалившийся тег — те же правила порядка, что вы видели на чистом движке.

Проверка: теги ShouldBindUri

Какие теги нужны полю структуры для ShouldBindUri?

Свой тег и StructLevel внутри gin#

Всё из уроков десять и одиннадцать работает в gin через официальную дверь — метод Engine() интерфейса StructValidator, который возвращает внутренний validator.Validate. Приведение типа any к validator.Validate с проверкой ok — и дальше обычный API: RegisterValidation для своих тегов, RegisterStructValidation для правил на всю структуру, RegisterTagNameFunc, если хотите видеть в ошибках json-имена. Регистрируйте до запуска сервера: регистрации не потокобезопасны, а движок общий на все запросы — это тот же контракт, что у одинокого экземпляра из второго урока. RegisterTagNameFunc уместен и здесь: json-имена в FieldError понадобятся respondFields, чтобы ключи конверта совпали с тем, что видит фронтенд, — регистрируйте его на том же экземпляре из Engine(), вместе со своими тегами. Отдельно решите, где живут ваши теги: если движок достаётся из binding.Validator, теги видны и ShouldBindJSON, и структурным правилам, и всему, что вы проверяете руками; если создаёте второй экземпляр через validator.New() — это отдельный мир со своим словарём, и его пересечение с gin-овским равно нулю. Про цену второго экземпляра и про то, почему валидатор один на приложение, — целиком следующий урок, там и продолжим.

Чек-лист gin binding validation#

  • правила — в теге binding: SetTagName переключил словарь движка на него
  • ShouldBindJSON = декод + валидация; статус 400 ставит ваш хендлер
  • ошибка данных — validator.ValidationErrors, достаётся errors.As
  • массив верхнего уровня — binding.SliceValidationError: отдельная ветка errors.As
  • ShouldBindUri: uri — имя параметра, binding — правила; имя совпадает с сегментом маршрута
  • конверт 400 — respondFields по Field/Tag/Param, единый на все ShouldBind-ветки
  • свои теги и StructLevel — через binding.Validator.Engine(), регистрация до старта сервера
  • второй validator.New() — отдельный словарь и пустой кэш: не создавайте в хендлерах

Соберите разбор ошибки в хендлере

Четыре пропуска: метод привязки, errors.As, статус и имя поля.

var in SignupDTO if err := c.(&in); err != nil { var verrs validator.ValidationErrors if errors.(err, &verrs) { for _, e := range verrs { fmt.Println(e.()) } c.JSON(http., gin.H{"errors": "проверьте поля"}) return } c.String(http.StatusBadRequest, "битый JSON") }

Соберите путь binding-тега

POST /signup с битым телом. Расставьте шаги по порядку.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: validator в gin

Термин

binding.Validator

нажмите, чтобы перевернуть
Определение

Пакетная переменная gin, через которую фреймворк говорит с движком. Интерфейс StructValidator: ValidateStruct(obj any) error и Engine() any.

1 / 6

Следующий урок — Echo, chi и свой Validation-хук: те же правила в фреймворках без встроенного валидатора, где конвейер bind → validate собираете вы. Разбор ошибок FieldError с методами Field, Tag и Param — в пятом уроке, цена одного экземпляра движка — в четырнадцатом. Весь путь binding — от ShouldBindJSON до массивов — есть в курсе Gin. Код урока запускается локально: gin и validator — внешние модули, песочница сайта исполняет только чистый Go, поэтому хендлеры проверяйте через go run и curl.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • ShouldBindJSON = декод JSON плюс валидация: движок v10 уже внутри gin, словарь переключён на binding через SetTagName.
  • Ошибки данных — validator.ValidationErrors: та же errors.As и FieldError, что на голом валидаторе; статус 400 ставит ваш хендлер.
  • Семейство ShouldBind различается источником: body, query, header, uri — теги раскладки разные, валидация binding одна.
  • ShouldBindUri: uri-тег задаёт имя параметра, binding — правила; uuid и oneof отсекают мусор URL до бизнес-логики.
  • Массив верхнего уровня даёт binding.SliceValidationError — отдельная ветка errors.As с индексами виновников.
  • Свои теги и StructLevel подключаются через binding.Validator.Engine() до старта сервера.
  • validate-теги в gin мертвы: молчаливый пропуск правил — главная грабля переезда структур.

Домашнее задание

Перенесите SignupDTO из одиннадцатого урока в gin-проект: добавьте маршрут POST /signup с ShouldBindJSON, структурное правило passwordmatch через binding.Validator.Engine() и маршрут GET /orders/:id/:status с ShouldBindUri и тегами uuid, oneof. Соберите respondFields по каталогу сообщений и проверьте curl-ом три сценария: валидное тело, пустой пароль, битый uuid в параметрах — убедитесь, что все три отвечают 400 с деталями по полям.

Личные заметкиПусто
Хранятся только в вашем браузере (Validator · Валидация в Gin: binding)

Вопросы и ответы по уроку

Почему ShouldBindJSON не возвращает 400 сам?

Он возвращает ошибку, а решение о статусе остаётся хендлеру: один проект хочет 400 с картой полей, другой — 422, третий — свой формат. Принятое разделение: валидатор решает, что данные плохи, фреймворк и ваш код решают, как об этом сказать. Главное — не потерять ошибку: ветка без обработки ошибок отправит нулевую структуру в бизнес-слой. Практика сверх того: фиксированная константа статуса на весь проект и единая функция-конверт — расхождение 400 и 422 между эндпоинтами читается клиентом как разные классы проблем, хотя это один и тот же провал валидации.

Как добраться до движка, чтобы зарегистрировать свой тег?

Через binding.Validator.Engine(): метод возвращает any, приведите к *validator.Validate с проверкой ok и вызывайте RegisterValidation или RegisterStructValidation. Делайте это до запуска сервера: регистрации не потокобезопасны. Если приведение не сработало — кто-то уже подменил binding.Validator, и лучше упасть на старте, чем жить без тега.

Что вернёт ShouldBindJSON на массиве верхнего уровня?

Для []TodoInput gin сам ходит по элементам и собирает ошибки в binding.SliceValidationError — срез error-ов со строками [0]:, [1]:. errors.As на validator.ValidationErrors не совпадёт: держите отдельную ветку. Внутри структуры срезы валидируются обычным движком с dive — там ошибки приезжают стандартным типом. Планируете принимать пачки часто — заведите для SliceValidationError свою ветку в respondFields: пройдитесь по элементам, достаньте ValidationErrors каждого и соберите карту «индекс → поля»; клиент получит подсветку по позициям массива без повторной валидации.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Asynq25 уроков~11 чпрактика25 уроков уже доступны

Asynq в Go: очередь фоновых задач на Redis

Курс asynq для Go: клиенты и воркеры, отложенные и периодические задачи, ретраи, очереди с приоритетами, агрегация, Inspector и asynqmon. 25 уроков.

  • Строить клиент и воркер asynq и подключать Redis
  • Отправлять отложенные, периодические и уникальные задачи
  • Настраивать ретраи, таймауты и приоритеты очередей
  • Мониторить задачи через Inspector и asynqmon