PostgreSQL com pgx em Go: o driver nativo
Como usar o driver pgx v5 no modo nativo do PostgreSQL, com prepared statements automáticos, pipelines e alta vazão em aplicações Go.
Publicada em · 8 min de leitura
Durante quase uma década, a maioria dos projetos em Go que precisavam se comunicar com o PostgreSQL utilizava o driver github.com/lib/pq. No entanto, em 2020, os mantenedores oficiais do lib/pq declararam o projeto em modo permanente de manutenção, recomendando que novas aplicações migrassem para soluções modernas.
O substituto que se consolidou como o padrão ouro absoluto da indústria para PostgreSQL em Go é o pgx (github.com/jackc/pgx/v5), criado e mantido por Jack Christensen.
O pgx não é apenas um driver comum: ele é um toolkit completo de alta performance que implementa o protocolo nativo de rede do PostgreSQL, suportando comunicação binária pura, tipos avançados nativos (JSONB, UUID, arrays), inserções massivas em lote via protocolo COPY e um pool de conexões próprio (pgxpool) muito superior à camada genérica do database/sql.
Esta aula ensina a operar o pgx v5 em seu modo nativo de máxima performance, eliminando o overhead de texto e alcançando taxas de vazão de dezenas de milhares de operações por segundo.
O Mapa da Aula
| Parte | Seção | O que você vai dominar |
|---|---|---|
| 1. A Evolução dos Drivers | 1 e 2 | Por que o lib/pq ficou para trás e a diferença entre pgx/stdlib e o modo nativo pgxpool |
| 2. Superpoderes do Protocolo Binário | 3 e 4 | Eliminação do parsing de strings, tipos avançados (arrays, JSONB, UUID) e cache automático de queries |
| 3. Inserções em Lote de Alta Vazão | 5 e 6 | pgx.Batch (pipeline de rede em 1 round-trip) vs pgx.CopyFrom (protocolo COPY com +100.000 ops/s) |
| 4. Produtividade com pgx v5 | 7 | Mapeamento automático sem reflexão pesada usando pgx.CollectRows e pgx.RowToStructByName |
| 5. Código de Produção | 8 a 10 | Serviço completo de telemetria, sintonia fina de pgxpool.Config e checklist de performance |
Pré-requisitos recomendados: database-sql e Connection Pooling.
Parte 1: A Evolução dos Drivers PostgreSQL em Go
1. Por que o lib/pq foi aposentado?
O driver tradicional lib/pq foi pioneiro, mas sofre de limitações arquiteturais severas:
- Opera exclusivamente no formato de texto do PostgreSQL. Cada inteiro, float, booleano ou timestamp enviado ou recebido precisa ser serializado para texto (ASCII) pelo cliente e parseado de volta pelo servidor, desperdiçando ciclos de CPU.
- Não implementa novos recursos do protocolo do PostgreSQL (como suporte nativo a pipelines, cancelamento de queries sem abrir uma nova conexão TCP e prepared statements automáticos).
- Está congelado: não recebe novos recursos de performance há anos.
2. As Duas Faces do pgx v5
O pgx oferece duas formas de uso:
1. Modo Compatível (github.com/jackc/pgx/v5/stdlib)
Permite usar o pgx por baixo da interface tradicional database/sql. É a opção utilizada quando você precisa conectar ferramentas como GORM ou bibliotecas agnósticas de banco. Embora seja muito mais rápido que o lib/pq, ele ainda paga o custo de adaptação da interface genérica do Go.
2. Modo Nativo Puro (github.com/jackc/pgx/v5/pgxpool) — Recomendado
Ignora completamente o database/sql e fala diretamente com o socket do PostgreSQL através de structs nativas (pgx.Conn, pgxpool.Pool). Desbloqueia o suporte completo a protocolo binário, COPY em massa, Batching e mapeamento direto de structs.
Parte 2: Superpoderes do Modo Nativo
3. Comunicação Binária Direta
No protocolo de texto tradicional, quando você busca uma data ou número:
Postgres converte timestamp -> "2026-10-20 08:30:00.123456" -> Rede -> Go faz time.Parse()
No modo nativo do pgx, os dados trafegam na sua representação binária pura:
Postgres envia 8 bytes de inteiro de 64 bits -> Rede -> Go lê diretamente na memória
Isso reduz o consumo de CPU em até 30% a 50% em consultas de grandes volumes de linhas.
4. Tipos Avançados Nativos sem Malabarismos
O pgx entende os tipos do PostgreSQL nativamente, mapeando direto para tipos idiomáticos do Go:
// Struct aproveitando tipos avançados do Postgres
type Dispositivo struct {
ID uuid.UUID // Mapeia para tipo UUID nativo
Tags []string // Mapeia para tipo TEXT[] nativo do Postgres
Metadata map[string]any // Mapeia direto para JSONB sem precisar de []byte intermediário
IPRede netip.Prefix // Mapeia para tipo CIDR / INET
}
5. Cache Automático de Statements (Automatic Statement Caching)
No database/sql, para preparar uma query você precisa chamar explicitamente db.Prepare().
O pgx faz isso de forma totalmente transparente: sempre que você executa uma query repetida via pool.Query ou pool.Exec, ele prepara a query no PostgreSQL na primeira vez e guarda o statement compilado no cache daquela conexão. Nas próximas execuções, o PostgreSQL pula a fase de análise sintática e planejamento da query, executando o plano já compilado na memória.
Parte 3: Inserções Massivas de Alta Performance
Quando seu sistema precisa persistir grandes lotes de registros (como logs de auditoria, eventos de telemetria ou importações de planilhas), fazer um loop simples de INSERT é um desastre de performance:
O pgx oferece duas ferramentas especializadas para resolver esse gargalo:
6. Pipeline de Rede com pgx.Batch
Em chamadas normais, cada query espera a resposta chegar pela rede antes de enviar a próxima. Se a latência de rede for de 2ms, inserir 1.000 registros levará pelo menos 2 segundos ($1.000 \times 2\text{ms}$).
O pgx.Batch empacota dezenas de queries e as envia juntas em um único pacote de rede (Pipeline), processando todas em um único round-trip:
batch := &pgx.Batch{}
// Enfileira dezenas de inserts na memória
for _, item := range itens {
batch.Queue("INSERT INTO pedidos (id, valor) VALUES ($1, $2)", item.ID, item.Valor)
}
// Despacha o lote inteiro de uma só vez pela rede
br := pool.SendBatch(ctx, batch)
defer br.Close()
// Lê os resultados
for range itens {
_, err := br.Exec()
if err != nil {
return err
}
}
- Performance: Passa de ~500 registros/s para mais de 15.000 registros por segundo.
7. Velocidade Extrema com pgx.CopyFrom
Se você precisa de performance absoluta (ex: carregar centenas de milhares de linhas em poucos segundos), o pgx implementa o protocolo nativo COPY do PostgreSQL:
linhas := make([][]any, len(eventos))
for i, e := range eventos {
linhas[i] = []any{e.ID, e.Dispositivo, e.Valor, e.CreatedAt}
}
colunas := []string{"id", "dispositivo", "valor", "created_at"}
// Inserção em streaming contínuo direto no mecanismo de armazenamento
linhasAfetadas, err := pool.CopyFrom(
ctx,
pgx.Identifier{"telemetria"},
colunas,
pgx.CopyFromRows(linhas),
)
- Por que é tão rápido? O comando
COPYignora o parser SQL comum do banco e envia um stream de dados brutos diretamente para as páginas de disco da tabela. - Performance: Ultrapassa facilmente 100.000 inserções por segundo.
Parte 4: Mapeamento Moderno no pgx v5
No database/sql, ler 10 colunas exigia chamar rows.Scan(&c1, &c2, &c3...) manualmente linha por linha. O pgx v5 introduziu os utilitários de alta produtividade pgx.CollectRows e pgx.RowToStructByName:
type Usuario struct {
ID int `db:"id"`
Nome string `db:"nome"`
Email string `db:"email"`
}
// 1. Executa a query
rows, err := pool.Query(ctx, "SELECT id, nome, email FROM usuarios WHERE ativo = true")
if err != nil {
return nil, err
}
// 2. Mapeia automaticamente todas as linhas para um slice []Usuario
// E fecha o objeto rows automaticamente no final!
usuarios, err := pgx.CollectRows(rows, pgx.RowToStructByName[Usuario])
if err != nil {
return nil, err
}
O pgx.CollectRows cuida do loop for rows.Next(), faz o fechamento seguro com rows.Close() e valida rows.Err() internamente, reduzindo 15 linhas de código repetitivo para uma única chamada tipada com Generics.
Parte 5: Código Prático Completo de Produção
Abaixo está a implementação completa de um repositório corporativo de ingestão de telemetria utilizando pgxpool, pgx.Batch, pgx.CopyFrom e consultas com mapeamento automático de structs.
O Serviço de Telemetria (telemetria.go):
package telemetria
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type EventoTelemetria struct {
ID string `db:"id"`
Dispositivo string `db:"dispositivo"`
Metrica string `db:"metrica"`
Valor float64 `db:"valor"`
CreatedAt time.Time `db:"created_at"`
}
type TelemetriaRepository struct {
pool *pgxpool.Pool
}
func NovoTelemetriaRepository(ctx context.Context, dsn string) (*TelemetriaRepository, error) {
// 1. Sintonia fina do pool nativo do pgx
cfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, fmt.Errorf("erro na DSN: %w", err)
}
cfg.MaxConns = 30 // Teto máximo de conexões
cfg.MinConns = 5 // Mantém conexões mínimas sempre aquecidas
cfg.MaxConnLifetime = 30 * time.Minute // Reciclagem periódica preventiva
cfg.MaxConnIdleTime = 5 * time.Minute // Encerra sockets sem uso
cfg.HealthCheckPeriod = 1 * time.Minute // Ping preventivo em segundo plano
// 2. Inicialização do pool
pool, err := pgxpool.NewWithConfig(ctx, cfg)
if err != nil {
return nil, fmt.Errorf("falha ao criar pgxpool: %w", err)
}
// 3. Validação real de conexão
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("postgres indisponível: %w", err)
}
return &TelemetriaRepository{pool: pool}, nil
}
func (r *TelemetriaRepository) Close() {
r.pool.Close()
}
// InserirLoteComBatch processa eventos usando pipeline de rede (1 round-trip)
func (r *TelemetriaRepository) InserirLoteComBatch(ctx context.Context, eventos []EventoTelemetria) error {
batch := &pgx.Batch{}
query := `INSERT INTO telemetria (id, dispositivo, metrica, valor, created_at) VALUES ($1, $2, $3, $4, $5)`
for _, e := range eventos {
batch.Queue(query, e.ID, e.Dispositivo, e.Metrica, e.Valor, e.CreatedAt)
}
br := r.pool.SendBatch(ctx, batch)
defer br.Close()
for range eventos {
_, err := br.Exec()
if err != nil {
return fmt.Errorf("erro ao processar item do batch: %w", err)
}
}
return nil
}
// InserirMassivoComCopyFrom atinge taxas de +100.000 inserts/segundo via protocolo COPY
func (r *TelemetriaRepository) InserirMassivoComCopyFrom(ctx context.Context, eventos []EventoTelemetria) (int64, error) {
linhas := make([][]any, len(eventos))
for i, e := range eventos {
linhas[i] = []any{e.ID, e.Dispositivo, e.Metrica, e.Valor, e.CreatedAt}
}
colunas := []string{"id", "dispositivo", "metrica", "valor", "created_at"}
linhasAfetadas, err := r.pool.CopyFrom(
ctx,
pgx.Identifier{"telemetria"},
colunas,
pgx.CopyFromRows(linhas),
)
if err != nil {
return 0, fmt.Errorf("falha no CopyFrom: %w", err)
}
return linhasAfetadas, nil
}
// BuscarEventosPorDispositivo utiliza pgx.CollectRows para mapeamento automático e seguro
func (r *TelemetriaRepository) BuscarEventosPorDispositivo(ctx context.Context, dispositivo string) ([]EventoTelemetria, error) {
query := `SELECT id, dispositivo, metrica, valor, created_at FROM telemetria WHERE dispositivo = $1 ORDER BY created_at DESC LIMIT 100`
rows, err := r.pool.Query(ctx, query, dispositivo)
if err != nil {
return nil, fmt.Errorf("falha na query: %w", err)
}
// CollectRows garante o fechamento do rows e mapeia direto para slice tipado!
eventos, err := pgx.CollectRows(rows, pgx.RowToStructByName[EventoTelemetria])
if err != nil {
return nil, fmt.Errorf("falha no mapeamento: %w", err)
}
return eventos, nil
}
Parte 6: Checklist de Boas Práticas com pgx
- Abandone o
lib/pqem novos projetos: Use semprepgx/v5. - Prefira
pgxpool.Pooladatabase/sql: A menos que você seja obrigado a usar uma biblioteca que exija a interface genérica do Go, usepgxpoolpara ter suporte a tipos nativos, binário direto eCopyFrom. - Use
pgx.Batchpara dezenas/centenas de inserts epgx.CopyFrompara milhares/milhões: Nunca faça laços depool.Exec()um a um. - Aproveite
pgx.CollectRows: Elimine o código repetitivo derows.Scane previna bugs de vazamento de sockets. - Configure
HealthCheckPeriod: Nopgxpool.Config, ative oHealthCheckPeriod(ex: 1 minuto) para que o pool teste conexões ociosas em segundo plano e descarte conexões caídas antes que suas rotinas de API tentem usá-las.
Exercício Prático Proposto
- Crie uma tabela
logs_auditoriacom camposid(UUID),tags(TEXT[]) edetalhes(JSONB). - Utilize o contêiner de testes PostgreSQL que aprendemos em Testes de Integração com Testcontainers.
- Escreva um benchmark comparando:
- Inserção de 1.000 logs usando loop de
pool.Execindividual. - Inserção dos mesmos 1.000 logs usando
pgx.Batch. - Inserção de 50.000 logs usando
pgx.CopyFrom.
- Inserção de 1.000 logs usando loop de
- Observe a diferença brutal de tempo e vazão no terminal!