Валидация данных и 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 читать слишком много данных.
Чеклист
- Backend валидирует ввод независимо от frontend.
- Пустые строки проходят через
strings.TrimSpace. - Query parameters парсятся и ограничиваются.
- Ошибки полей возвращаются в стабильном формате.
- UUID парсится и проверяется до использования.
- Технические ошибки не отдаются пользователю напрямую.
- Бизнес-валидация не спрятана в слишком магические теги.
- Ограничены длины строк, размеры файлов и лимиты списков.