Опирается на правила:
R-SQLC-POOL-1…R-SQLC-POOL-5иR-SQLC-POOL-X1…R-SQLC-POOL-X3из sqlc Rules → раздел 5. pgxpool и соединение.
Важно знать
pgxpool.Pool— singleton. Создаётся один раз при старте и инжектируется конструктором во всё, что работает с БД.pgxpool.ParseConfig+pgxpool.NewWithConfig— единственный правильный путь настройки. Прямойpgxpool.New(ctx, dsn)не позволяет задатьMaxConns,MinConns,HealthCheckPeriod.- DSN читается из переменных окружения через
envconfigилиos.Getenv. Хардкод строки подключения в коде — нарушение.pool.Ping(ctx)при старте — fail-fast: приложение не должно подниматься без БД и падать на первом запросе пользователя.pool.Close()вызывается вdeferилиgracefulShutdownпри остановке сервиса.pgx.Connect(одиночное соединение) — не для production: нет пула, нет автоматического reconnect при разрыве.- Глобальная переменная
var pool *pgxpool.Pool— запрещена. Инжекция только через конструктор.
pgxpool.Pool — это менеджер соединений к PostgreSQL в pgx/v5. Он держит пул переиспользуемых соединений, возвращает их при ошибках, выполняет health-check и настраивается отдельно от строки DSN. Всё приложение работает с одним инстансом пула — создание соединений per-request или per-repository-method убивает производительность и исчерпывает лимит соединений базы данных.
Настройка через ParseConfig и NewWithConfig
R-SQLC-POOL-1, R-SQLC-POOL-2: пул создаётся один раз через pgxpool.ParseConfig + pgxpool.NewWithConfig, не напрямую через строку DSN.
// infrastructure/postgres.go
package infrastructure
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
type Config struct {
DSN string `envconfig:"DATABASE_URL" required:"true"`
MaxConns int `envconfig:"DB_MAX_CONNS" default:"20"`
MinConns int `envconfig:"DB_MIN_CONNS" default:"2"`
MaxConnLifetime time.Duration `envconfig:"DB_MAX_CONN_LIFETIME" default:"1h"`
}
func NewPool(cfg Config) (*pgxpool.Pool, error) {
poolCfg, err := pgxpool.ParseConfig(cfg.DSN)
if err != nil {
return nil, fmt.Errorf("parse pool config: %w", err)
}
poolCfg.MaxConns = int32(cfg.MaxConns)
poolCfg.MinConns = int32(cfg.MinConns)
poolCfg.MaxConnLifetime = cfg.MaxConnLifetime
poolCfg.HealthCheckPeriod = 30 * time.Second
pool, err := pgxpool.NewWithConfig(context.Background(), poolCfg)
if err != nil {
return nil, fmt.Errorf("create pool: %w", err)
}
if err := pool.Ping(context.Background()); err != nil {
return nil, fmt.Errorf("ping db: %w", err)
}
return pool, nil
}
Почему не pgxpool.New(ctx, dsn):
- Нет контроля над параметрами пула.
pgxpool.Newпринимает только строку DSN; настройкиMaxConns,MinConns,MaxConnLifetimeзадаются черезpgxpool.Config— объект, который отдаёт толькоParseConfig. - Разные отвественности. DSN описывает куда соединяться (
host,port,dbname,sslmode). Параметры пула описывают сколько и как долго. Разделение черезParseConfig+ поля конфига делает эти две плоскости явными. - Переопределение без магических строк. В тесте
poolCfg.MaxConns = 5понятнее, чем добавление?pool_max_conns=5в строку DSN и надежда, что DSN правильно разберётся.
DSN из переменных окружения
R-SQLC-POOL-3: строка подключения никогда не хардкодится. Для сервисов Сбер (sber-order-service, sber-payment-service) это особенно критично — DSN содержит учётные данные.
// infrastructure/config.go
package infrastructure
import "github.com/kelseyhightower/envconfig"
func LoadConfig() (Config, error) {
var cfg Config
if err := envconfig.Process("", &cfg); err != nil {
return Config{}, fmt.Errorf("load config: %w", err)
}
return cfg, nil
}
# .env.example (без реальных значений)
DATABASE_URL=postgres://user:password@localhost:5432/orders?sslmode=disable
DB_MAX_CONNS=20
DB_MIN_CONNS=2
DB_MAX_CONN_LIFETIME=1h
Обёртка envconfig.Process читает поля структуры через теги envconfig: и применяет дефолты через default:. Это аккуратнее ручного os.Getenv для каждого поля и даёт встроенную валидацию обязательных полей через required:"true".
Ping при старте — fail-fast
R-SQLC-POOL-4: pool.Ping(ctx) сразу после создания пула — обязательный шаг.
// cmd/api/main.go
func main() {
cfg, err := infrastructure.LoadConfig()
if err != nil {
log.Fatalf("load config: %v", err)
}
pool, err := infrastructure.NewPool(cfg.DB)
if err != nil {
log.Fatalf("init db pool: %v", err)
}
defer pool.Close()
// ... инициализация репозиториев, handler-ов, HTTP-сервера
}
Что даёт Ping при старте:
- Ранний сбой виден сразу. Если переменная
DATABASE_URLуказывает на несуществующий хост или неверный пароль, приложение упадёт при деплое, а не при первом запросе пользователя через 10 минут. - Kubernetes readiness. В паре с readiness probe (
/health) Ping гарантирует, что Pod помечается Ready только когда БД доступна и пул инициализирован. - Ошибка с контекстом.
fmt.Errorf("ping db: %w", err)даёт полную цепочку: оператор видитinit db pool: ping db: failed to connect to ... (FATAL: password authentication failed), а не голыйconnection refused.
В production-инфраструктуре Сбер pool.Ping с timeout-контекстом и логом IP-адреса целевой реплики позволяет диагностировать routing-проблемы между DC без liveness-рестартов:
func NewPool(cfg Config) (*pgxpool.Pool, error) {
// ... ParseConfig, NewWithConfig ...
pingCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := pool.Ping(pingCtx); err != nil {
return nil, fmt.Errorf("ping db (host=%s): %w", cfg.Host(), err)
}
return pool, nil
}
Инжекция через конструктор
R-SQLC-POOL-1, R-SQLC-POOL-X2: пул инжектируется в репозиторий конструктором. Глобальная переменная и init-функция — запрещены.
// adapters/out/persistence/postgres_order_repository.go
package persistence
import (
"github.com/jackc/pgx/v5/pgxpool"
"order-service/db"
)
type PostgresOrderRepository struct {
q *db.Queries
pool *pgxpool.Pool
}
func NewPostgresOrderRepository(pool *pgxpool.Pool) *PostgresOrderRepository {
return &PostgresOrderRepository{
q: db.New(pool),
pool: pool,
}
}
// adapters/out/persistence/postgres_customer_repository.go
type PostgresCustomerRepository struct {
q *db.Queries
pool *pgxpool.Pool
}
func NewPostgresCustomerRepository(pool *pgxpool.Pool) *PostgresCustomerRepository {
return &PostgresCustomerRepository{
q: db.New(pool),
pool: pool,
}
}
Каждый репозиторий получает тот же синглтон *pgxpool.Pool, созданный в main. db.New(pool) создаёт *db.Queries — сгенерированный sqlc-объект, который умеет работать как с pgxpool.Pool, так и с pgx.Tx (через q.WithTx(tx)). Второе поле pool нужно для BulkInsertItems через pgx.CopyFrom и для Begin при открытии транзакции на handler-е.
Почему не глобальная переменная:
- Тест не может подменить соединение. Глобал зафиксирован в compile-time. Конструктор принимает интерфейс или
*pgxpool.Pool— в тесте передаётся пул от Testcontainers. - Нет порядка инициализации.
init()не гарантирует порядок между пакетами. Репозиторий из другого пакета, сконструированный доinit()pool-пакета, получит nil-пул. - Явная зависимость лучше скрытой. Подпись
NewPostgresOrderRepository(pool *pgxpool.Pool)документирует требование: «для работы нужен пул». Глобал скрывает это.
Закрытие пула при остановке сервиса
R-SQLC-POOL-5: pool.Close() вызывается при graceful shutdown.
// cmd/api/main.go
func main() {
pool, err := infrastructure.NewPool(cfg.DB)
if err != nil {
log.Fatalf("init db pool: %v", err)
}
defer pool.Close() // закрытие при выходе из main
srv := &http.Server{
Addr: cfg.Addr,
Handler: router,
}
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("server shutdown: %v", err)
}
}
pool.Close() дожидается освобождения активных соединений и корректно закрывает все установленные TCP-сессии. Без этого при рестарте PostgreSQL получает обрывы соединений вместо корректных FIN-пакетов, что замедляет отработку idle_in_transaction_session_timeout на стороне БД.
Подробнее про порядок остановки — в graceful shutdown pgxpool.
Параметры пула — как выбирать значения
Три параметра влияют на поведение пула в production:
MaxConns — верхний предел соединений. Типичный выбор: pg_stat_activity-лимит делённый на число реплик сервиса. Для Сбер-сервисов с 10 репликами и лимитом 200 соединений: MaxConns = 15 с запасом. Дефолт pgx/v5 — 4, что заниженно для production.
MinConns — минимальное число прогретых соединений. Значение 2–3 устраняет latency-spike при первых запросах после старта. Для Product-сервиса с редким трафиком ночью MinConns = 1 снижает потребление соединений.
MaxConnLifetime — максимальное время жизни соединения. Рекомендуется ниже idle_in_transaction_session_timeout PostgreSQL (обычно 1–2 часа). Это защищает от ситуации, когда pgbouncer или балансировщик закрывают соединение со своей стороны, а pgx считает его живым.
HealthCheckPeriod — интервал проверки живости соединений в пуле. Значение 30s — разумный баланс между своевременным обнаружением мёртвых соединений и нагрузкой на БД.
poolCfg.MaxConns = int32(cfg.MaxConns) // 20 для высоконагруженных сервисов
poolCfg.MinConns = int32(cfg.MinConns) // 2 для прогрева
poolCfg.MaxConnLifetime = cfg.MaxConnLifetime // 1h
poolCfg.HealthCheckPeriod = 30 * time.Second // 30s
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
pgx.Connect(ctx, dsn) в production-коде | R-SQLC-POOL-X1 | pgxpool.NewWithConfig — пул с reconnect |
var pool *pgxpool.Pool как глобальная переменная | R-SQLC-POOL-X2 | Инжекция через конструктор NewPostgresOrderRepository(pool) |
pgxpool.New(ctx, dsn) без ParseConfig | R-SQLC-POOL-2 | pgxpool.ParseConfig + поля poolCfg.MaxConns и т.д. |
DSN строкой в коде ("postgres://user:pass@...") | R-SQLC-POOL-3 | envconfig с тегом envconfig:"DATABASE_URL" |
Пропуск pool.Ping(ctx) при старте | R-SQLC-POOL-X3 | pool.Ping с timeout-контекстом; сбой = log.Fatalf |
Нет pool.Close() при остановке | R-SQLC-POOL-5 | defer pool.Close() в main или graceful-handler |
Куда дальше
- Repository pattern на Go — как
PostgresOrderRepositoryполучает пул и как строить интерфейс-порт вcore/. - Транзакции на Go — как
pool.Begin(ctx)используется на handler-е,WithTx(tx)иdefer tx.Rollback. - Graceful shutdown — БД и persistence — порядок остановки: HTTP → активные транзакции →
pool.Close().