Содержание урока · 10 разделов
- Драйвер pgx: пустой импорт и sql.Open
- OpenDB и OpenDBFromPool: два конструктора
- Нативный pgx против database/sql поверх него
- Что остаётся от pgx под интерфейсом
- Raw(): люк обратно в нативный pgx
- Когда stdlib оправдан одним списком
- Миграция на нативный pgx: по шагам
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Урок, которого могло бы не быть, если бы вся экосистема Go писала на нативных драйверах. Но вокруг database/sql вырос слой кода, который принимает именно sql.DB: ORM GORM с драйвером postgres, генераторы, утилиты миграций, корпоративные обёртки с метриками, и подключить pgx к этому миру приходится через адаптер stdlib. Второй сценарий — чужой репозиторий: вам достался сервис на database/sql, и переписывать всё под нативный API никто не бюджетировал; stdlib позволяет сменить драйвер на pgx одной строкой импорта и получить его скорость, не трогая ни один запрос. Третий — библиотеки: если вы пишете пакет для чужих сервисов и не хотите навязывать базу, интерфейс database/sql остаётся разумным контрактом. Первый урок курса мы проговорили это с другой стороны — теперь посмотрим на адаптер вблизи: как он регистрируется, чем sql.DB отличается от pgxpool под капотом, где заканчивается совместимость и почему я считаю stdlib переходной станцией, а не постоянным домом для прикладного сервиса.
Драйвер pgx: пустой импорт и sql.Open#
Адаптер живёт в подпакете stdlib того же модуля github.com/jackc/pgx/v5. Его функция init регистрирует драйвер под двумя именами — pgx и pgx/v5 — поэтому достаточно пустого импорта с подчёркиванием, и sql.Open("pgx", dsn) начинает работать с любым DSN из третьего урока: postgres://-строкой или keyword=value-формой. Секундная ошибка новичка: забыть пустой импорт — тогда sql.Open возвращает ошибку unknown driver «pgx» (forgotten import?), и текст прямо подсказывает диагноз. Если нужна тонкая настройка, которую не выразить строкой подключения, — pgx.ParseConfig даёт *pgx.ConnConfig, и её можно либо разыменовать и передать в stdlib.OpenDB напрямую, либо зарегистрировать через stdlib.RegisterConnConfig: функция вернёт служебную строку подключения, по которой sql.Open найдёт именно эту конфигурацию. Наконец, stdlib.GetDefaultDriver() отдаёт сам объект драйвера — путь для sql.OpenDB с собственным driver.Connector. Все четыре входа ведут к одному адаптеру; различается только то, кто владеет настройками соединений — строка DSN, конфиг или внешний pgxpool, о котором ниже.
Привычная программа на database/sql
GOpackage main
import (
"database/sql"
"fmt"
"log"
_ "github.com/jackc/pgx/v5/stdlib"
)
func main() {
db, err := sql.Open("pgx",
"postgres://app:secret@localhost:5432/app?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
var title string
err = db.QueryRow(
"SELECT title FROM notes WHERE id = $1", 1).Scan(&title)
fmt.Println(title, err)
}Пустой импорт stdlib регистрирует драйвер pgx, дальше — стандартный код database/sql: Open, QueryRow, Scan. Контекст здесь не обязателен — у database/sql есть и версии с суффиксом Context, и без него, в отличие от нативного pgx, где ctx — часть каждой сигнатуры. Обратите внимание: драйвер ленивый, поэтому реальное соединение откроется только на первом запросе.
Первое, что бросается в глаза после перехода с нативного кода, — мелкая грамматика. Ошибка пустого результата здесь sql.ErrNoRows из database/sql, а не pgx.ErrNoRows; нулевые типы принято брать sql.NullString и компанию из шестого урока; а соединениями управляет собственный пул database/sql с SetMaxOpenConns и SetMaxIdleConns — pgxpool из четвёртого урока в этой схеме не участвует. У стандартного пула свои приоритеты: он честно держит N соединений открытыми и переиспользует их, но у него нет MaxConnLifetime с джиттером, нет готовой статистики Stat и ленивого прогрева — те мелочи, которые pgxpool делает из коробки. Prepared statements управляются самим database/sql: интерфейс умеет Stmt, но драйверные нюансы кэширования прячутся внутри — db.Prepare вернёт *sql.Stmt для многократного переиспользования, тогда как нативный pgx строит кэш сам и обычному прикладному коду о нём думать не даёт. Ни одно из этих отличий не страшно, но все они означают, что часть знаний курса про пул и жизненный цикл коннектов к адаптеру не применяется напрямую — вы работаете по правилам стандартной библиотеки, и книгу, которую вы сейчас читаете, придётся на время закрыть.
OpenDB и OpenDBFromPool: два конструктора#
stdlib.OpenDB(config pgx.ConnConfig) собирает sql.DB из полной конфигурации pgx — путь, когда настройки не лезут в DSN: кастомный Tracer из двадцать четвёртого урока, тонкая настройка TLS, свой Logger. Сюда же относятся параметры уровня коннекта вроде application_name и search_path — они живут в RuntimeParams той же конфигурации; хуки BeforeConnect и AfterConnect — собственность pgxpool: при OpenDBFromPool они остаются в пуле и продолжают срабатывать для каждого нового коннекта. Второй конструктор появился в v5.5 и решает деликатную задачу совместимости двух пулов: stdlib.OpenDBFromPool(pool pgxpool.Pool) делает sql.DB, чьи соединения — это коннекты вашего pgxpool. Без него два пула делили бы лимит max_connections базы, не зная друг о друге; с ним один pgxpool обслуживает и нативный код, и всё, что требует sql.DB. Механика честная: database/sql при каждом запросе Acquire-ит коннект из pgxpool и Release-ит после, поэтому стандартному пулу запрещают собственные idle-соединения — OpenDBFromPool сам выставляет SetMaxIdleConns(0), чтобы не выгрызать коннекты у pgxpool. Владение разделено: закрытие *sql.DB не закрывает пул — кто-то один в приложении должен вызвать pool.Close(), и обычно это код, который этот пул создал. Это же правило делает OpenDBFromPool главным мостом для постепенной миграции: новые методы пишутся на нативном pgx, старые продолжают ходить через интерфейс, и оба мира делят одну обслугу.
Нативный pgx против database/sql поверх него#
| Возможность | Нативный pgx | Через stdlib |
|---|---|---|
| Скорость драйвера и протокола | полная | та же — драйвер один и тот же |
| Batch и пайплайнинг | да | нет — интерфейс не знает про пачки |
| COPY FROM для массовой загрузки | да | только через Raw() |
| LISTEN/NOTIFY | да | нет |
| Типы pgtype без адаптеров | да | через pgtype.NewMap().SQLScanner |
| Контекст в каждой сигнатуре | обязателен | опционален — есть версии без Context |
| Коллекторы CollectRows и RowTo | да | нет — обобщений в интерфейсе нет |
Проверка: что теряется
Какая возможность нативного pgx недоступна через чистый интерфейс database/sql?
Что напечатает программа?
Прочитайте код и выберите, что он выведет:
package main
import (
"database/sql"
"fmt"
"log"
)
func main() {
db, err := sql.Open("pgx",
"postgres://app:secret@localhost:5432/app")
if err != nil {
log.Fatal(err)
}
fmt.Println(err, db != nil)
}Что остаётся от pgx под интерфейсом#
Термин «вы теряете скорость» в контексте stdlib вводит в заблуждение: драйвер — тот же самый, и весь нативный протокол с бинарной передачей значений работает под интерфейсом как есть. Проигрыш против нативного кода начинается на слое обёртки: database/sql конвертирует значения через интерфейсы и отражения, где pgx кладёт байты прямо в приёмники, — это не «разы», но и не ноль на горячем пути. Что правда живёт и здесь: режимы исполнения запросов. По умолчанию адаптер использует QueryExecModeCacheStatement — кэш prepared statements на каждую соединение; при pgbouncer в режиме transaction pooling режим переключают на QueryExecModeSimpleProtocol или Exec через конфиг, и это работает через stdlib так же, как через нативный pgx. Доступ к нативным типам из pgtype остаётся: pgtype.NewMap().SQLScanner(&target) оборачивает pgtype-приёмник в sql.Scanner для стандартного интерфейса — с ним массивы и NUMERIC сканируются и в *sql.DB. Наконец, ошибки на дороге: pgconn.PgError доходит и через database/sql — его вытаскивают из цепочки через errors.As, так что разбор SQLSTATE из шестнадцатого урока применим в обоих мирах. Забавная деталь свежих версий: с 5.7 pgx.ErrNoRows сам оборачивает sql.ErrNoRows, поэтому старые проверки на sql.ErrNoRows продолжают срабатывать и в нативном коде — мост между мирами проложен с обеих сторон. Потеря происходит не в скорости и не в типах, а в выразительности: пачки, NOTIFY, коллекторы и контексты в сигнатурах — интерфейс их просто не описывает.
Raw(): люк обратно в нативный pgx#
Когда под интерфейсом понадобилась нативная операция — COPY, SendBatch, LISTEN, — database/sql предлагает штатный механизм для таких «выходов из абстракции»: (sql.Conn).Raw(). Схема из трёх шагов. Сначала db.Conn(ctx) арендует отдельное соединение из стандартного пула — его надо вернуть явно. Затем conn.Raw(func(driverConn any) error { ... }): внутри колбэка driverConn приводится к stdlib.Conn, и метод .Conn() отдаёт настоящий *pgx.Conn, на котором работает весь нативный API. Внутри колбэка запрещено держать ссылку на соединение после выхода — Raw декорирует соединение на время вызова, и использование после возврата — гонка, которую ревью должно ловить. После колбэка conn.Close() возвращает соединение пулу. Люк делает карту «stdlib как постоянный дом» несостоятельной: массовые операции всё равно пишутся нативно, а значит, половина команды всё равно должна владеть нативным API. Практический вывод, к которому я веду весь урок: Raw() — отличный инструмент миграции и редких исключений, но если нативных операций в проекте больше двух-трёх, честнее признать, что дом проекта — нативный pgx, а stdlib — гостиница для legacy.
Raw() + CopyFrom: массовая вставка из-под интерфейса
GOconn, err := db.Conn(ctx)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
err = conn.Raw(func(driverConn any) error {
pconn := driverConn.(*stdlib.Conn).Conn()
_, err := pconn.CopyFrom(
ctx,
pgx.Identifier{"notes"},
[]string{"title", "body"},
pgx.CopyFromRows(rowsData),
)
return err
})db.Conn берёт соединение из стандартного пула, Raw декорирует его на время колбэка, а приведение к stdlib.Conn отдаёт нативный pgx.Conn — дальше работает обычный CopyFrom из девятнадцатого урока. После возврата из Raw соединение обязано использоваться только через database/sql: держать ссылку на *pgx.Conn после выхода — гонка.
Проверка: люк в нативный API
Как из *sql.DB получить *pgx.Conn для нативной операции?
Спорное мнение, которое я держу с первого урока: держать прикладной сервис на stdlib ради гипотетической смены СУБД — убыток. Что сломалось бы при таком переезде на деле: SQL-диалект, типы, миграции и тесты, а интерфейс драйвера — последняя из проблем; зато каждый день вы платите потерей Batch, NOTIFY и коллекторов. Условие, при котором я меняю позицию: вы поставляете библиотеку, которую подключают чужие сервисы, — там контракт database/sql честнее навязывания базы. Второе честное условие — поверх стоит GORM или ent, которые просят *sql.DB: драйвер postgres у GORM и ent под капотом и так pgx, так что это вопрос не скорости, а владения конфигурацией.
Когда stdlib оправдан одним списком#
- Миграция унаследованного кода: смена драйвера одной строкой импорта без правки запросов
- Библиотека или SDK для чужих сервисов, где нельзя навязывать конкретную СУБД
- Зависимости, требующие *sql.DB: ORM, генераторы, корпоративные обёртки с метриками
- Постепенный переезд на нативный API с общим pgxpool через OpenDBFromPool
- Тестовые утилиты и CLI-инструменты, где вся мощь драйвера не нужна
- Во всех остальных случаях — нативный pgx: быстрее в написании, богаче в возможностях
Соберите подключение через stdlib
Восстановите пустой импорт и открытие базы.
Соберите запрос через database/sql
Расставьте строки работы с *sql.DB в правильном порядке.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: pgx и database/sql
Миграция на нативный pgx: по шагам#
Когда решение принято, переезд на нативный API предсказуем. Шаг первый — соединения: sql.DB заменяется на pgxpool.Pool из четвёртого урока, SetMaxOpenConns переезжает в MaxConns конфига пула. Шаг второй — грамматика вызовов: у всех методов появляется обязательный контекст, Exec и Query меняют подписи местами, а QueryRow теряет суффиксы Context. Шаг третий — ошибки и типы: sql.ErrNoRows становится pgx.ErrNoRows, sql.NullString — pgtype.Text из шестого урока, а для NUMERIC и UUID появляются честные типы — о них десятый. Шаг четвёртый — проверки: ошибки соединений теперь приходят с нативными типами, и errors.As на pgconn.PgError заменяет разбор через driver.ErrBadConn. Пятый шаг — самое приятное: ручные циклы схлопываются в CollectRows, а место под Batch и COPY освобождается само. Опыт команд, которые это проходили, совпадает: неделя на механику, остальное — на удаление обёрток, которые были нужны только из-за интерфейса. Держите оба мира на одном pgxpool через OpenDBFromPool, пока миграция идёт — тогда лимит соединений базы никогда не станет сюрпризом посреди релиза.
Напоминание про запуск: песочница сайта исполняет тематическую программу на чистом Go без сети — stdlib-код из урока требует локального Go и Postgres из первого урока, и именно там интерактивы этого раздела превращаются в работающие команды. По курсу дальше: в десятом уроке — типы Postgres и pgtype, которые через адаптер доступны с оговорками, а нативно — во всей красе; в девятнадцатом — COPY, ради которого в этом уроке появился люк Raw(). Если после этого урока остался один вывод — «stdlib для чужого кода, нативный для своего», — материал сработал: это ровно та развилка, с которой начинается любой разговор о драйвере в команде.
Драйвер postgres для GORM и ent через database/sql — под ним тот же pgx через stdlib. Понимание этого урока объясняет, почему ORM-конфиги принимают DSN в формате pgx и почему трассировка драйвера видна в ORM-логах: слои не дублируются, а складываются в один стек.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Пустой импорт pgx/stdlib регистрирует драйвер pgx для sql.Open("pgx", ...) — ошибка forgotten import выдаёт забытый импорт.
- Скорость драйвера сохраняется: под интерфейсом тот же нативный протокол; теряются Batch, COPY, NOTIFY и коллекторы.
- stdlib.OpenDB(config) для тонких настроек, OpenDBFromPool(pool) — *sql.DB поверх pgxpool с Acquire/Release.
- OpenDBFromPool выставляет SetMaxIdleConns(0), и db.Close() не закрывает пул — пулом владеет создатель.
- Raw() даёт люк в нативный pgx: *stdlib.Conn → .Conn() → CopyFrom; держать ссылку после Raw нельзя.
- Ошибки в stdlib-мире — sql.ErrNoRows; pgconn.PgError доходит и через database/sql через errors.As.
- Миграция на нативный pgx — недели механики: контексты, pgtype, ошибки, коллекторы.
Домашнее задание
Возьмите программу из этого урока и прогоните её дважды: через sql.Open("pgx", ...) и через pgxpool с нативным pgx — сравните сигнатуры и почувствуйте разницу в контекстах. Затем соберите гибрид: pgxpool.OpenDBFromPool поверх пула из четвёртого урока, один метод списка на database/sql и один нативный с CollectRows — убедитесь, что оба мира делят один пул. В конце воспроизведите ошибку forgotten import, убрав пустой импорт, и запишите её текст: этот сюрприз вы ещё встретите в чужом коде.
Личные заметкиПусто
Вопросы и ответы по уроку
Медленнее ли pgx через database/sql, чем нативный?
Драйверная часть — та же, протокол и бинарные значения работают одинаково. Разница появляется на слое обёртки database/sql: конвертация значений через интерфейсы и отражения. На типовых запросах это единицы процентов; на горячих путях нативный pgx выигрывает заметнее, плюс получает Batch и коллекторы, которых у интерфейса нет.
Нужен ли отдельный пул соединений при OpenDBFromPool?
Нет, и в этом смысл конструктора: соединения берутся из вашего pgxpool через Acquire и возвращаются Release-ом. Стандартный пул database/sql при этом работает без собственных idle-соединений — конструктор выставляет SetMaxIdleConns(0) сам, чтобы не выгрызать коннекты у pgxpool. Закрывать нужно пул, а не *sql.DB.
Как через stdlib сканировать UUID или NUMERIC?
Стандартный путь — pgtype.NewMap().SQLScanner(&target): адаптер превращает pgtype-приёмник в sql.Scanner для database/sql. Простейшие случаи — UUID в строку — работают и без адаптера, но для массивов, NUMERIC и NULL-семантики pgtype честнее, и код не разваливается на два стиля.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
MongoDB в Go: официальный драйвер mongo-driver v2
Курс MongoDB для Go: подключение, bson-теги, CRUD, фильтры, агрегации, индексы, транзакции и Change Streams. 25 уроков по mongo-driver v2.
- Подключаться к MongoDB и настраивать клиента
- Моделировать документы структурами и bson-тегами
- Делать CRUD, сложные фильтры и проекции
- Строить агрегации $match/$group/$lookup