Pular para o conteúdo

Trilha de Go · parte 19 de 37

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

Tamanho da fonte:
Largura da coluna:

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

ParteSeçãoO que você vai dominar
1. A Evolução dos Drivers1 e 2Por 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ário3 e 4Eliminaçã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ão5 e 6pgx.Batch (pipeline de rede em 1 round-trip) vs pgx.CopyFrom (protocolo COPY com +100.000 ops/s)
4. Produtividade com pgx v57Mapeamento automático sem reflexão pesada usando pgx.CollectRows e pgx.RowToStructByName
5. Código de Produção8 a 10Serviç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

comparativo drivers postgres
figura 1 · comparativo drivers postgres

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:

mecanica batch copyfrom
figura 2 · mecanica batch copyfrom

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 COPY ignora 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

  1. Abandone o lib/pq em novos projetos: Use sempre pgx/v5.
  2. Prefira pgxpool.Pool a database/sql: A menos que você seja obrigado a usar uma biblioteca que exija a interface genérica do Go, use pgxpool para ter suporte a tipos nativos, binário direto e CopyFrom.
  3. Use pgx.Batch para dezenas/centenas de inserts e pgx.CopyFrom para milhares/milhões: Nunca faça laços de pool.Exec() um a um.
  4. Aproveite pgx.CollectRows: Elimine o código repetitivo de rows.Scan e previna bugs de vazamento de sockets.
  5. Configure HealthCheckPeriod: No pgxpool.Config, ative o HealthCheckPeriod (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

  1. Crie uma tabela logs_auditoria com campos id (UUID), tags (TEXT[]) e detalhes (JSONB).
  2. Utilize o contêiner de testes PostgreSQL que aprendemos em Testes de Integração com Testcontainers.
  3. Escreva um benchmark comparando:
    • Inserção de 1.000 logs usando loop de pool.Exec individual.
    • Inserção dos mesmos 1.000 logs usando pgx.Batch.
    • Inserção de 50.000 logs usando pgx.CopyFrom.
  4. Observe a diferença brutal de tempo e vazão no terminal!

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?