Go: GORM
Теория: Валидация и ошибки
GORM берёт на себя генерацию SQL и маппинг структур, но ответственность за обработку ошибок всё равно остаётся на приложении. Код должен понимать, что произошло: записи просто нет, данные не подходят, база недоступна или миграция не прошла. От этого зависят и HTTP-коды, и сообщения пользователю, и то, откатывать ли транзакцию.
В этом уроке разберём три уровня:
- как интерпретировать ошибки GORM (
ErrRecordNotFound,ErrInvalidDataи не только); - как проверять входные данные до запроса к базе;
- как аккуратно работать с ошибками миграций и транзакций, чтобы база всегда оставалась в согласованном состоянии.
Проверка ошибок: ErrRecordNotFound и ErrInvalidData
GORM почти всегда возвращает результат операции через объект *gorm.DB. В нём есть:
Error— сама ошибка;RowsAffected— сколько строк реально затронуто.
Частый сценарий — выборка, которая ничего не нашла. Для этого случая у GORM есть специальная ошибка gorm.ErrRecordNotFound. Это не «катастрофа», а нормальный исход: пользователь с таким ID просто не существует.
Другой типовой случай — в GORM передали некорректную модель. Например, попытались создать nil или обновить структуру без первичного ключа. Такие вещи обозначаются ошибкой gorm.ErrInvalidData.
Отличаем «не найдено» от «сломалось»
Пример аккуратного чтения записи:
Важно:
- для сравнения используем
errors.Is(), а не сравнение текстов сообщений; - ошибку оборачиваем через
%w, чтобы верхний уровень всё ещё мог узнать, что внутри былErrRecordNotFound.
Используем RowsAffected для массовых операций
Если вы делаете массовый UPDATE или DELETE, ErrRecordNotFound не появится — запрос корректен, просто не нашёл подходящих строк. В этом случае смотрят на RowsAffected:
Такой код явно фиксирует ситуацию «операция ничего не изменила» и может превратить её в 422/404, а не тихо делать вид, что всё прошло успешно.
ErrInvalidData: некорректная модель на входе
Некоторые ошибки GORM появляются ещё до обращения к базе — ORM проверяет, что вы вообще дали ей что-то разумное.
Примеры:
Идея простая: если модель не позволяет построить осмысленный запрос, GORM возвращает ErrInvalidData, и это повод посмотреть на ваш код, а не на базу.
Проверка входных данных до запроса
Базу лучше воспринимать как «последнюю линию обороны». Всё, что можно проверить до неё, нужно проверять до неё: обязательные поля, формат email, длина пароля, диапазоны чисел, бизнес-правила уровня процесса.
Паттерн удобно строить вокруг DTO: отдельных структур для входных данных с методами нормализации и валидации.
DTO: нормализуем и валидируем запрос
Простейший пример — регистрация пользователя:
Сервисный слой:
В этом подходе:
- вход нормализуется и проверяется до вызова GORM;
- ошибки маппятся на
ErrInvalidData, чтобы контроллер мог отдать400/422; - сама модель (
User) может дополнительно проверять себя в хуках (например, хэшировать пароль).
Числовые диапазоны и бизнес-инварианты
Более содержательный пример — создание заказа:
Здесь на уровне DTO сразу отсеиваются:
- пустой покупатель;
- пустой список товаров;
- нулевое или отрицательное количество;
- отрицательная цена.
После такой проверки до базы доходят только осмысленные заказы. При этом ключевые правила всё равно дублируют в схеме (NOT NULL, CHECK, UNIQUE), чтобы защититься от гонок и «обхода» правил другим кодом.
Простейшая защита от мусора: проверка ID
Даже для простого GET /users/{id} не стоит сразу бежать в базу. Нулевой или отрицательный ID можно отбрасывать на входе:
Такой код:
- сразу отбрасывает заведомо некорректный запрос;
- различает «не найдено» и «сломалось».
Обработка ошибок миграций и транзакций
Миграции и транзакции — точки, где ошибка особенно опасна: можно оставить схему в полуприменённом состоянии или часть данных обновлённой, а часть — нет. Задача кода — либо довести операцию до конца, либо откатить всё, не оставляя «пола на две плитки».
Ошибки миграций: если не получилось — падаем
AutoMigrate() и Migrator() работают поверх DDL-запросов (CREATE TABLE, ALTER TABLE и т. д.). Любая ошибка здесь должна остановить сервис: лучше не стартовать, чем работать на сломанной схеме.
Минимальный шаблон:
Точечные изменения через Migrator():
Любая ошибка оборачивается и пробрасывается выше. Приложение не продолжает работу «как ни в чём не бывало».
Миграции данных в транзакции
Если нужно не только изменить схему, но и подготовить данные, всё это заворачивается в одну транзакцию:
Если любой шаг внутри блока вернёт ошибку, GORM сделает ROLLBACK, и база останется в исходном состоянии.
Ошибки в транзакциях: различаем причины
Типичный паттерн работы с транзакцией:
Транзакция:
- либо успешно завершится (
COMMIT); - либо вернёт ошибку, и GORM сделает
ROLLBACK.
Важно, что вы чётко различаете:
- «нет записи» (
ErrRecordNotFound); - «условия операции не выполнены» (
ErrInvalidData); - «что-то сломалось» (драйвер, тайм-аут, дедлок и т. д.).
Контекст и тайм-ауты
Ещё одна типовая причина падения транзакций — запрос просто висит слишком долго. Правильно ограничивать время выполнения через context:
Если контекст истечёт:
- драйвер вернёт ошибку;
- GORM откатит транзакцию;
- соединение освободится.
Так вы не держите блокировки и пул соединений бесконечно.
«Дефер-откат» и паники
При ручном управлении транзакцией шаблон почти всегда один:
Даже если вы выйдете из функции раньше или словите панику, Rollback() из defer снимет блокировки и вернёт соединение в пул.



