Содержание

Валидация данных и UUID в Go

Валидация - это проверка данных до того, как программа начнет им доверять. Пользователь может отправить пустое имя, отрицательный год, слишком длинный текст, неправильный UUID, файл не того типа или JSON с лишними полями. Внешний API тоже может вернуть неожиданные данные.

Хороший backend не предполагает, что вход всегда корректный. Он проверяет данные на границе системы, возвращает понятные ошибки и не пропускает мусор глубже в бизнес-логику.

UUID помогает выдавать объектам стабильные идентификаторы, которые не зависят от порядка создания записей и не раскрывают количество объектов в системе.

Что валидировать

Проверяйте все, что приходит извне:

  • HTTP body;
  • query parameters;
  • path parameters;
  • CLI flags;
  • переменные окружения;
  • CSV, JSON, YAML файлы;
  • данные из внешнего API;
  • сообщения из очереди;
  • загружаемые файлы.

Даже если frontend уже проверяет форму, backend должен проверить ее повторно. Frontend можно обойти: открыть DevTools, отправить curl, изменить запрос через proxy или вызвать API из другого клиента.

Validation vs business rules

Есть простая валидация:

  • поле обязательно;
  • строка не длиннее 100 символов;
  • year не отрицательный;
  • email похож на email;
  • UUID имеет правильный формат.

Есть бизнес-правила:

  • пользователь может оставить только один отзыв на книгу;
  • нельзя удалить опубликованный счет;
  • скидка доступна только активным студентам;
  • email должен быть уникальным.

Простую валидацию можно держать рядом с input struct. Бизнес-правила обычно живут в service layer, потому что могут требовать базу, текущего пользователя или состояние системы.

Простая ручная валидация

type CreateBookInput struct { Title string Author string Year int } func (input CreateBookInput) Validate() error { if strings.TrimSpace(input.Title) == "" { return errors.New("title is required") } if strings.TrimSpace(input.Author) == "" { return errors.New("author is required") } if input.Year < 0 { return errors.New("year must be positive") } return nil }

Для небольших проектов ручная валидация часто понятнее, чем библиотека. В коде явно видно правило и текст ошибки.

strings.TrimSpace

Пустая строка и строка из пробелов должны обрабатываться одинаково:

if strings.TrimSpace(input.Title) == "" { return errors.New("title is required") }

Без TrimSpace пользователь может отправить " ", и формально строка будет не пустой.

Ошибки по полям

Для API удобнее возвращать ошибки по полям:

type ValidationError struct { Fields map[string]string `json:"fields"` }

Пример ответа:

{ "error": { "code": "validation_failed", "message": "Проверьте поля формы", "fields": { "title": "required", "year": "must be positive" } } }

Frontend сможет подсветить конкретные поля, а не показывать одну общую ошибку.

Не раскрывайте внутренние детали

Пользовательская ошибка должна быть понятной:

invalid id

А не такой:

pq: invalid input syntax for type uuid: "abc"

Технические детали можно оставить в логах. API-ответ должен быть стабильным и безопасным.

UUID

UUID удобен как публичный ID:

id := uuid.NewString()

Преимущества:

  • сложно угадать соседний ID;
  • можно генерировать до записи в БД;
  • удобно передавать между сервисами;
  • не раскрывает количество записей;
  • подходит для событий и idempotency keys.

Проверка UUID

Если ID приходит из path parameter, сначала распарсьте его:

id, err := uuid.Parse(rawID) if err != nil { return errors.New("invalid id") }

Не передавайте непроверенный ID прямо в бизнес-логику. Чем раньше вы отклоните неправильный формат, тем меньше странных ошибок получите ниже.

UUID и база данных

В PostgreSQL можно использовать тип UUID:

CREATE TABLE books ( id UUID PRIMARY KEY, title TEXT NOT NULL );

UUID можно генерировать в приложении или в базе. Для микросервисов и событий часто удобно генерировать в приложении: id уже известен до записи.

Библиотеки валидации

Для больших API можно использовать validation-библиотеки, где правила задаются тегами:

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

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

Валидация query parameters

Query parameters приходят строками:

limitRaw := r.URL.Query().Get("limit") limit, err := strconv.Atoi(limitRaw) if err != nil || limit <= 0 || limit > 100 { http.Error(w, "invalid limit", http.StatusBadRequest) return }

Не используйте значение напрямую. limit=1000000 может случайно заставить API читать слишком много данных.

Чеклист

  1. Backend валидирует ввод независимо от frontend.
  2. Пустые строки проходят через strings.TrimSpace.
  3. Query parameters парсятся и ограничиваются.
  4. Ошибки полей возвращаются в стабильном формате.
  5. UUID парсится и проверяется до использования.
  6. Технические ошибки не отдаются пользователю напрямую.
  7. Бизнес-валидация не спрятана в слишком магические теги.
  8. Ограничены длины строк, размеры файлов и лимиты списков.

Следующий шаг после статьи

Закрепите тему в реальном проекте и продолжайте обучение на курсах Praxis.

Продолжить изучение

Выбери следующую статью по маршруту или углубись в смежную тему.

Похожие статьи