Работа с базой в Go: database/sql, pgx и sqlc — сравниваем варианты
В Go нет единого «правильного» способа работать с PostgreSQL. Стандартная библиотека database/sql, драйвер pgx и кодогенератор sqlc решают одну задачу тремя разными путями — с разной ценой в производительности, удобстве и сопровождении.

Зачем три инструмента для одной базы
Любой Go-сервис, который ходит в PostgreSQL, начинается с выбора: использовать стандартный интерфейс database/sql, подключить pgx напрямую или отдать маппинг генератору sqlc. Каждый вариант тянет за собой цепочку последствий — от скорости запроса до того, как выглядит код ревью через полгода. Ниже — разбор трёх подходов с реальными фрагментами кода и таблицей компромиссов.
database/sql: стандартная библиотека
Пакет database/sql из стандартной библиотеки Go — это абстракция над любым SQL-драйвером. Он управляет пулом соединений, поддерживает транзакции, prepared statements и интерфейс sql.Scanner для маппинга строк.
Что получаете
- Единый API для любой СУБД: PostgreSQL, MySQL, SQLite — меняется только драйвер.
- Встроенный пул соединений с настройками
SetMaxOpenConns,SetMaxIdleConns,SetConnMaxLifetime. - Никаких внешних зависимостей помимо драйвера.
Чем платите
Ручной маппинг через rows.Scan — основная боль. Каждый запрос требует перечисления полей в том же порядке, в каком они идут в SELECT. Добавили колонку — обновите каждый Scan вручную. Компилятор не поможет: несовпадение порядка — это runtime error, а не ошибка сборки.
Второй минус — потеря PostgreSQL-специфичных типов. database/sql оперирует обобщёнными интерфейсами, и чтобы работать с jsonb, массивами или inet, придётся писать кастомные сканеры или тянуть обёртки из того же pgx.
// Go 1.22+, драйвер: github.com/jackc/pgx/v5/stdlib
package main
import (
"context"
"database/sql"
"fmt"
"log"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
)
type Order struct {
ID int64
UserID int64
Total int64 // в копейках
CreatedAt time.Time
}
func ordersByUser(ctx context.Context, db *sql.DB, userID int64) ([]Order, error) {
const query = `SELECT id, user_id, total, created_at
FROM orders
WHERE user_id = $1
ORDER BY created_at DESC`
rows, err := db.QueryContext(ctx, query, userID)
if err != nil {
return nil, fmt.Errorf("query orders: %w", err)
}
defer rows.Close()
var result []Order
for rows.Next() {
var o Order
// порядок полей должен точно совпадать с SELECT
if err := rows.Scan(&o.ID, &o.UserID, &o.Total, &o.CreatedAt); err != nil {
return nil, fmt.Errorf("scan order: %w", err)
}
result = append(result, o)
}
return result, rows.Err()
}
Код рабочий, но хрупкий: если в SELECT появится пятое поле, а Scan не обновят — паника или тихое смещение значений.
pgx: прямой драйвер для PostgreSQL
pgx (актуальная мажорная версия — v5) — это и драйвер, совместимый с database/sql через пакет stdlib, и самостоятельная библиотека с собственным API. Второй режим интереснее: он даёт доступ к возможностям PostgreSQL, которые стандартная библиотека не предоставляет.
Ключевые отличия от database/sql
- Собственный пул —
pgxpool.Poolуправляет соединениями без прослойкиdatabase/sql. Меньше аллокаций, поддержкаLISTEN/NOTIFY,COPY. - Batch-запросы —
pgx.Batchотправляет несколько запросов одним раундтрипом. Для микросервисов с частыми мелкими запросами это экономия десятков миллисекунд. - Нативные типы —
pgtypeподдерживаетjsonb,uuid,inet,int4rangeи прочие PostgreSQL-типы без костылей. - Маппинг через
pgx.CollectRows— методpgx.RowToStructByNameсканирует строку в структуру по именам полей, а не по порядку. Это убирает главную проблемуdatabase/sql.
// pgx v5, Go 1.22+
package main
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type Order struct {
ID int64 `db:"id"`
UserID int64 `db:"user_id"`
Total int64 `db:"total"`
CreatedAt time.Time `db:"created_at"`
}
func ordersByUser(ctx context.Context, pool *pgxpool.Pool, userID int64) ([]Order, error) {
const query = `SELECT id, user_id, total, created_at
FROM orders
WHERE user_id = $1
ORDER BY created_at DESC`
rows, err := pool.Query(ctx, query, userID)
if err != nil {
return nil, fmt.Errorf("query orders: %w", err)
}
// CollectRows сам закрывает rows
orders, err := pgx.CollectRows(rows, pgx.RowToStructByName[Order])
if err != nil {
return nil, fmt.Errorf("collect orders: %w", err)
}
return orders, nil
}
Кода меньше, маппинг по именам, а не по позиции. Но SQL по-прежнему живёт строковым литералом — компилятор его не проверяет.
Если ваш сервис работает только с PostgreSQL и переезд на другую СУБД не планируется — нет практического смысла оставаться на обобщённом интерфейсе database/sql. pgx v5 даёт и скорость, и удобство, и доступ ко всему, что умеет Postgres.
sqlc: SQL пишете вы, код генерирует машина
sqlc работает иначе: вы пишете обычные SQL-запросы в .sql-файлах, описываете схему (или указываете на живую базу), а sqlc generate создаёт типизированный Go-код — структуры, методы, маппинг. Опечатка в имени колонки ломает генерацию, а не runtime.
Как устроен цикл
- Описать схему в
schema.sqlили подключить миграции. - Написать запросы в
query.sqlс аннотациями-- name: GetOrdersByUser :many. - Запустить
sqlc generate— получить файлы.goс готовыми функциями. - Вызывать сгенерированные методы из бизнес-логики.
-- query.sql (входной файл для sqlc)
-- name: GetOrdersByUser :many
SELECT id, user_id, total, created_at
FROM orders
WHERE user_id = $1
ORDER BY created_at DESC;
После sqlc generate (версия sqlc 1.26+) появится примерно такой код:
// Сгенерировано sqlc — не редактируйте вручную
func (q *Queries) GetOrdersByUser(ctx context.Context, userID int64) ([]Order, error) {
// ...
}
Вызов из сервиса — одна строка: orders, err := queries.GetOrdersByUser(ctx, userID). Никаких Scan, никаких строковых литералов SQL в бизнес-логике.
Подводные камни
- Динамические запросы — sqlc не умеет генерировать код для запросов, которые собираются из частей в рантайме (фильтры по десяти необязательным параметрам). Для таких случаев придётся держать параллельно pgx или query builder вроде
squirrel. - Сложный SQL — CTE с рекурсией,
LATERAL JOIN, оконные функции парсятся, но иногда sqlc не выводит типы корректно. Приходится дописыватьsqlc.arg()и кастить руками. - Привязка к одной СУБД — sqlc поддерживает PostgreSQL, MySQL и SQLite, но синтаксис аннотаций и поведение различаются; переключение между СУБД — почти переписывание.
Сноска. Query builder — библиотека, которая собирает SQL-запрос программно (цепочкой вызовов), а не строковой конкатенацией. В Go популярны squirrel и goqu.
Таблица сравнения
| Критерий | database/sql | pgx v5 (прямой API) | sqlc + pgx |
|---|---|---|---|
| Проверка SQL при сборке | Нет | Нет | Да — при генерации |
| Ручной маппинг (Scan) | Да, по позиции | Нет — RowToStructByName | Нет — генерируется |
| Динамические запросы | Строки или builder | Строки или builder | Только статичные; для остального — fallback на pgx |
| Batch-запросы | Нет | Да | Нет (пока нет поддержки в sqlc) |
| Поддержка LISTEN/NOTIFY | Нет | Да | Нет |
| Переносимость между СУБД | Высокая | Только PostgreSQL | Ограниченная |
| Количество бойлерплейта | Много | Среднее | Минимум |
| Внешние зависимости | Только драйвер | pgx | pgx + sqlc (CLI) |
Когда что выбирать
database/sql — если нужна переносимость
Библиотека или сервис, который должен работать и с PostgreSQL, и с SQLite (тесты на embedded-базе, например). Или если в команде строгий запрет на прямые зависимости от конкретного драйвера. На практике таких проектов немного: большинство Go-сервисов живут с одной СУБД.
pgx — если проект целиком на PostgreSQL
Сервис активно использует PostgreSQL-специфику: jsonb, массивы, LISTEN/NOTIFY, COPY, advisory locks. Или в проекте много динамических запросов, которые sqlc не покрывает. pgx — разумный выбор по умолчанию для нового Go + Postgres проекта.
sqlc + pgx — если SQL стабилен и команда большая
Основная выгода — контракт между SQL и Go проверяется до деплоя. В команде из пяти человек это означает, что ревью SQL-миграции автоматически ловит сломанные запросы: sqlc generate упадёт в CI. Для сервисов с десятками запросов к стабильной схеме экономия на бойлерплейте и багах маппинга оправдывает дополнительный шаг генерации.
Типичная комбинация в проде
Чистый выбор «только один инструмент» встречается реже, чем кажется. Распространённая схема:
- sqlc — для CRUD и отчётных запросов с фиксированной сигнатурой.
- pgx напрямую — для динамических фильтров, batch-запросов и всего, что sqlc не тянет.
- database/sql — для интеграционных тестов с SQLite, если нужна лёгкая база без контейнера.
Эти три уровня не конфликтуют: sqlc генерирует код, который внутри использует pgx, а pgx можно подключить к database/sql через пакет stdlib.
Что делать дальше
Выбрали подход — проверьте его на своей схеме, а не на todo-приложении. Возьмите три-пять реальных запросов из проекта: один простой SELECT, один с JOIN, один с опциональными фильтрами. Прогоните через каждый инструмент и посмотрите, сколько кода, где компилятор ловит ошибку, а где она доедет до прода.
Следующий шаг после выбора — настройка пула соединений. Дефолтные значения pgxpool рассчитаны на умеренную нагрузку; для highload стоит разобраться с MaxConns, MinConns, MaxConnLifetime и HealthCheckPeriod — тема для отдельного разбора. Для sqlc имеет смысл встроить sqlc generate в CI и ломать сборку, если сгенерированный код не совпадает с тем, что лежит в репозитории: это защитит от забытой перегенерации после изменения схемы.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: pgx — PostgreSQL driver and toolkit for Go (GitHub), sqlc — официальная документация
Частые вопросы
Можно ли использовать sqlc и pgx одновременно в одном проекте?
Что если я обновил схему базы, а sqlc generate забыли запустить?
pgx v5 быстрее database/sql при одинаковых запросах?
Что делать, если sqlc не может вывести тип для сложного CTE или оконной функции?
Как тестировать код, написанный на pgx, без живого PostgreSQL?
Что если у меня другая версия pgx — v4?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.