Go: SQL
Теория: Введение в sqlc
sqlc — это инструмент, который превращает SQL-запросы в готовый Go-код. Он читает .sql-файлы, анализирует запросы и на их основе генерирует функции, структуры и типы. Сгенерированный код использует обычный пакет database/sql, поэтому его можно применять в любом Go-проекте без дополнительной магии. sqlc выступает мостом между «чистым SQL» и типобезопасным кодом на Go.
Главная идея sqlc проста. Пишите SQL руками, как в консоли или в миграциях, а генератор берёт на себя рутину: создаёт структуры для результатов, типы параметров, функции для вызова запросов и маппинг колонок в поля. Вместо ручного QueryContext(), Scan() и sql.NullString появляются методы вроде CreateUser() или GetUserByEmail() с нормальными Go-типами и проверкой на этапе компиляции.
Пример работы sqlc
Создадим в базе таблицу пользователей.
В файле query/users.sql описываются запросы, которые нужно сгенерировать.
Директива -- name говорит sqlc, как назвать функцию и какой контракт у запроса. Суффикс :one означает, что запрос должен вернуть одну строку, а при пустом результате будет ошибка.
После генерации sqlc создаёт структуры и методы.
Теперь работа с базой сводится к вызову обычных функций.
Здесь нет ручного Scan(), нет rows.Close(), нет риска перепутать порядок колонок или тип. Типы проверяются компилятором, а соответствие колонок и полей один раз фиксируется генератором.
Зачем нужен sqlc и чем он отличается от ручной работы
Ручная работа с database/sql даёт полный контроль над запросами. Код сам решает, когда открывать соединение, как профилировать сложный SQL и какие конструкции строить динамически. Но такой подход создаёт много шаблонного кода. Каждая выборка требует Scan(), каждое изменение — QueryRow() с ручным маппингом, а любая ошибка в порядке колонок или типе всплывает только в рантайме.
При «голом» database/sql программист описывает структуры, пишет SQL как строку, следит за колоночными позициями и руками обрабатывает NULL-значения.
С sqlc код сводится к одному вызову функции.
Внутри по-прежнему работает database/sql и тот же драйвер. Меняется только то, что типобезопасный слой сгенерирован автоматически и не требует ручного Scan.
sqlc сохраняет контроль над SQL. Запросы по-прежнему пишутся руками, без скрытой ORM. При этом инструмент добавляет типобезопасность: структура результата и типы параметров проверяются компилятором. Объём повторяющегося кода сокращается, а изменения в .sql-файлах после sqlc generate автоматически попадают в Go-слой.
Установка sqlc
Сам sqlc написан на Go, поэтому устанавливается через стандартный go install.
Эта команда скачивает исходники и собирает бинарь sqlc в каталог $GOPATH/bin или ~/go/bin, если используется путь по умолчанию. Чтобы инструмент был доступен из терминала, этот каталог добавляют в переменную PATH. Проверка установки выполняется через простой вызов.
В ответ отображается версия и список поддерживаемых языков. В контексте Go-проекта достаточно того, что среди них есть Go.
Базовая настройка и структура проекта
Конфигурация sqlc описывается в файле sqlc.yaml в корне проекта. Этот файл говорит генератору, где лежат миграции и запросы, какую СУБД использовать и в какой пакет складывать сгенерированный код.
Простейшая конфигурация для PostgreSQL выглядит так.
Версия указывает формат конфигурации. Блок sql — это список описаний схем, потому что в одном проекте можно генерировать код для нескольких баз данных. Каждый элемент списка связывает конкретную схему, набор запросов и параметры генерации.
Параметр engine задаёт тип СУБД. Для разных движков используются разные значения: postgresql, mysql, sqlite, sqlserver. Выбор определяет, как sqlc интерпретирует типы и особенности синтаксиса.
Поле schema указывает путь к SQL-файлам со схемой базы. Обычно сюда попадает каталог с миграциями. В этих файлах описываются таблицы, типы, индексы и внешние ключи. sqlc читает их, чтобы понять, какие SQL-типы нужно сопоставить Go-типам.
Поле queries описывает путь к SQL-файлам с запросами. Здесь лежат SELECT, INSERT, UPDATE и DELETE, снабжённые директивами -- name:. На основе этих файлов sqlc генерирует функции и структуры.
Блок gen.go управляет кодогенерацией. В нём задаётся имя пакета и путь, куда положить сгенерированные .go-файлы. Пакет попадёт в директиву package, а out определит каталог, внутри которого появятся db.go, models.go и файлы с реализацией запросов.
Типичная структура небольшого проекта с sqlc выглядит так.
В migrations хранятся файлы схемы. В query — запросы для разных сущностей. В internal/db sqlc складывает сгенерированный Go-код, который потом импортируется в сервис.
Директивы в запросах и тип результата
Комментарии вида -- name: внутри .sql-файлов управляют тем, какие функции и с какими контрактами создаст sqlc.
Суффикс :one означает, что запрос должен вернуть одну строку, и генератор создаст функцию, возвращающую структуру и ошибку. Суффикс :many указывает на множество строк, и тогда sqlc генерирует функцию, которая возвращает срез структур. Суффикс :exec нужен для команд, которые не возвращают данных, например для UPDATE или DELETE. Для подсчёта числа затронутых строк используется :execrows.
Генерация кода и использование в приложении
После настройки конфигурации и написания SQL остаётся запустить генерацию.
Инструмент прочитает файл sqlc.yaml, обработает схему и запросы и создаст Go-код в указанной директории. В пакет попадут модели для таблиц, обёртка над соединением и методы, соответствующие директивам -- name:.
Дальше код подключается к базе и использует сгенерированный пакет как обычный модуль доступа к данным.
Соединение остаётся обычным sql.DB. Методы, сгенерированные sqlc, вызываются как обычные функции, но скрывают внутри себя все детали Scan() и проверки типов.
Дополнительные настройки sqlc.yaml
Конфигурация sqlc не ограничивается базовыми полями. В блоке gen.go можно включать дополнительные опции, влияющие на удобство работы с кодом.
Опция emit_json_tags добавляет JSON-теги к полям структур. Это удобно, когда одни и те же типы используются и для работы с базой, и для выдачи ответов через HTTP или сериализации в JSON.
Опция emit_interface генерирует интерфейс Querier с методами, соответствующими запросам. Такой интерфейс пригодится для тестирования и внедрения зависимостей, потому что сервисы могут принимать Querier вместо конкретной реализации.
Опция emit_pointers_for_null_types позволяет получать *string и *int вместо sql.NullString и других «null»-типов. Этот режим удобен там, где выше по стеку данные всё равно конвертируются в указатели или в JSON-структуры с указателями.
Если названия SQL-типов пересекаются с ключевыми словами Go или не подходят под соглашения проекта, конфигурация поддерживает переименование. Для этого используется раздел rename, где можно задать новое имя для сгенерированного типа.
Проверка конфигурации перед генерацией выполняется командой sqlc vet. Эта команда анализирует схему, запросы и связи, но не создаёт код. После успешной проверки можно запускать sqlc generate.
Разделение на пакеты и директории
В простых проектах обычно достаточно одного пакета с доступом к базе и одного каталога с запросами. По мере роста системы удобнее делить схему и запросы по подсистемам и генерировать отдельные пакеты для каждой области.
Базовый вариант для небольшого сервиса выглядит так.
Когда появляются разные домены вроде пользователей, заказов и биллинга, файлы миграций разбивают по директориям. Для каждой подсистемы создаётся свой набор миграций и запросов, а в конфигурации — свой блок sql.
Конфигурация для нескольких пакетов описывается несколькими элементами списка в sqlc.yaml.
В таком варианте каждая подсистема получает свой пакет, свой набор моделей и функций и свою конфигурацию. Импорты между ними остаются явными, а границы ответственности — понятными.
Организация .sql-файлов
Файлы с запросами удобнее группировать по назначению. Один файл может отвечать за чтение, другой — за запись. Это упрощает навигацию и ревью.
В файле users_read.sql хранятся выборки и отчёты.
В файле users_write.sql собраны вставки и обновления.
Для сложных агрегатов с JOIN лучше заводить отдельные файлы и использовать явные алиасы для колонок. Тогда sqlc создаёт читаемые проекционные структуры.
Алиасы в SELECT должны совпадать с желаемыми именами полей результата. Это избавляет от «звёздочек» и делает изменения схемы управляемыми: изменение имени колонки отражается сначала в запросе, затем в сгенерированном типе.
Подведем итоги
sqlc даёт возможность продолжать писать чистый SQL и одновременно получать типобезопасный Go-код без ручного Scan() и маппинга. Конфигурация в sqlc.yaml описывает схему, запросы и пакеты, а структура директорий отражает доменную логику. Запросы снабжаются директивами -- name:, группируются по чтению и записи и оформляются с явными колонками и алиасами. После этого генерация сводится к одной команде, а доступ к данным — к набору обычных Go-функций, которые легко тестировать и использовать в продакшене.


