Go: SQL
Теория: Чтение данных через sqlc
В sqlc вся логика действительно начинается в .sql-файлах. Разработчик пишет обычный SQL, помечает запросы директивой -- name:, указывает форму результата через суффикс, а все структуры, параметры и методы генератор создаёт сам. Важно сразу договориться о стиле: только явные колонки вместо SELECT *, понятные алиасы под имена полей, стабильный порядок столбцов и плейсхолдеры $1, $2 и так далее для PostgreSQL. Такая дисциплина позволяет sqlc выводить точные типы и делает код устойчивым к изменениям схемы.
Для пользователей удобно завести каталог query/users и положить туда файл users.sql. В этом файле описываются запросы для одной доменной области. Сначала можно добавить короткий запрос, который создаёт запись и сразу возвращает всю строку. Комментарий -- name: задаёт имя будущей функции, а суффикс :one означает, что запрос вернёт одну строку.
После генерации появится структура User и метод CreateUser(ctx, params) с типобезопасными полями. Если столбец name допускает NULL, sqlc подберёт подходящий тип: sql.NullString или указатель — в зависимости от настроек конфигурации. В результате сигнатура функции будет соответствовать схеме, а ручной Scan() не понадобится.
Для чтения по естественному ключу запрос оформляют так, чтобы имена колонок совпадали с желаемыми полями структуры. Явные колонки и стабильный порядок избавляют от сюрпризов при миграциях и изменении таблиц.
Списки возвращаются в форме :many. Пагинация выражается через LIMIT и OFFSET, передаваемые параметрами, а порядок фиксируется индексируемым столбцом или датой создания. Такая форма делает вывод предсказуемым, а план запроса легко кэшируется СУБД.
Команды без возвращаемых данных помечаются как :exec. Этот контракт говорит генератору, что функция вернёт только ошибку. Если важно знать количество затронутых строк, используют :execrows, и сигнатура дополнится счётчиком. Выбор формы зависит от того, требуется ли проверка числа изменённых записей в бизнес-логике.
Для сложных выборок из нескольких таблиц имеет смысл завести отдельный файл под конкретную проекцию. Алиасы в SELECT должны совпасть с желаемыми полями результата. Тогда сгенерированная структура получается читаемой, а маппинг остаётся однозначным.
Когда требуется вернуть идентификатор сразу после вставки, конструкция с RETURNING остаётся лучшим выбором. Такой запрос выполняется атомарно, не требует второго похода в базу и легко типизируется sqlc.
Иногда параметры запроса оказываются двусмысленными для парсера, особенно в выражениях вроде count($1) или при сложных преобразованиях типов. В таких местах полезно явно приводить типы на стороне SQL, чтобы генератор однозначно понимал сигнатуру. В PostgreSQL явные касты через ::type решают проблему и не ухудшают производительность.
Условия с множеством значений удобно писать в PostgreSQL через = ANY($1) и передавать массив. sqlc по схеме понимает тип массива и генерирует параметр как слайс нужного базового типа. Это позволяет обойтись без динамического построения IN (...) и ручной склейки параметров.
Подготовка данных в CTE делает запросы понятнее и не мешает генерации. sqlc рассматривает итоговый SELECT как источник правды о форме результата и по нему строит тип. При этом сохраняется то же правило: явные имена столбцов и аккуратные алиасы держат код устойчивым к изменениям.
Все эти запросы хранятся в обычных .sql-файлах, сгруппированных по доменам и зонам ответственности. Один файл отвечает за конкретную область, один запрос описывает один контракт через -- name: и суффикс формы результата. Дальше остальное делает sqlc: читает схему из migrations, заходит в query//.sql, выводит точные Go-типы, создаёт методы и избавляет от ручного rows.Next() и Scan().
Генерация кода и подключение в Go
Генерацию запускает сам sqlc. Инструмент читает файл конфигурации sqlc.yaml, грузит схемы, разбирает запросы и по результатам складывает готовые .go-файлы в указанный пакет. После sqlc generate в папке, заданной в out, появляются файлы db.go, models.go и файлы вида users.sql.go.
В приложении остаётся открыть соединение через database/sql, настроить пул, проверить доступность базы и создать объект Queries из сгенерированного пакета. На его основе строится весь доступ к данным. Ниже показан пример для PostgreSQL и драйвера pgx/stdlib.
Сгенерированный код предоставляет методы, привязанные к контексту и типам схемы. Каждый вызов выглядит как бизнес-операция с понятной сигнатурой.
Транзакции оформляются через обычный *sql.Tx. Сначала открывается транзакция, затем на её основе создаётся «транзакционный» набор методов, и уже он вызывает сгенерированные функции. Такой подход сохраняет атомарность и не требует ручного Exec() и Scan().
При включённой опции emit_interface: true sqlc генерирует интерфейс Querier. Тогда сервисный слой принимает абстракцию, а не конкретный тип *Queries. В продакшене передаётся db.New(conn), в тестах — db.New(tx) или фейковая реализация.
Контекст и таймауты продолжают работать привычным образом. Каждый метод sqlc принимает ctx, поэтому долго выполняющиеся запросы можно ограничивать по времени и при необходимости отменять.
Генерация повторяется при любых изменениях .sql-файлов. Рабочий цикл остаётся простым: SQL правится в удобных файлах, затем запускается sqlc generate, после чего компилятор Go проверяет совместимость сигнатур. Изменение схемы не размазывается по всему коду, а отражается в одном месте — в SQL и сгенерированных типах.


