Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 9 из 25 ~16 мин +20 XP

Интерфейс database/sql: pgx через StdLib

pgx умеет жить внутри абстрактного интерфейса database/sql: адаптер stdlib регистрирует драйвер pgx под sql.Open, а поверх него работают *sql.DB, sql.NullString и всё, что ждёт от вас стандартная библиотека. Разберём, когда такой ход оправдан, что от pgx остаётся под интерфейсом, что безвозвратно теряется и как устроен люк Raw() обратно в нативный API.

Содержание урока · 10 разделов

Урок, которого могло бы не быть, если бы вся экосистема 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

GO
package 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: массовая вставка из-под интерфейса

GO
conn, 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 оправдан одним списком#

  • Миграция унаследованного кода: смена драйвера одной строкой импорта без правки запросов
  • Библиотека или SDK для чужих сервисов, где нельзя навязывать конкретную СУБД
  • Зависимости, требующие *sql.DB: ORM, генераторы, корпоративные обёртки с метриками
  • Постепенный переезд на нативный API с общим pgxpool через OpenDBFromPool
  • Тестовые утилиты и CLI-инструменты, где вся мощь драйвера не нужна
  • Во всех остальных случаях — нативный pgx: быстрее в написании, богаче в возможностях

Соберите подключение через stdlib

Восстановите пустой импорт и открытие базы.

import ( "database/sql" "github.com/jackc/pgx/v5/stdlib" ) func main() { db, err := sql.("pgx", "postgres://app:secret@localhost:5432/app") if err != nil { log.Fatal(err) } defer db.Close() }

Соберите запрос через database/sql

Расставьте строки работы с *sql.DB в правильном порядке.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: pgx и database/sql

Термин

pgx/stdlib

нажмите, чтобы перевернуть
Определение

Подпакет-адаптер: пустой импорт регистрирует драйвер pgx для sql.Open("pgx", ...). Тот же протокол под интерфейсом database/sql.

1 / 6

Миграция на нативный 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 для чужого кода, нативный для своего», — материал сработал: это ровно та развилка, с которой начинается любой разговор о драйвере в команде.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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: pgx через StdLib)

Вопросы и ответы по уроку

Медленнее ли 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 честнее, и код не разваливается на два стиля.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

MongoDB25 уроков~12 чпрактика25 уроков уже доступны

MongoDB в Go: официальный драйвер mongo-driver v2

Курс MongoDB для Go: подключение, bson-теги, CRUD, фильтры, агрегации, индексы, транзакции и Change Streams. 25 уроков по mongo-driver v2.

  • Подключаться к MongoDB и настраивать клиента
  • Моделировать документы структурами и bson-тегами
  • Делать CRUD, сложные фильтры и проекции
  • Строить агрегации $match/$group/$lookup