pgx: driver nativo para PostgreSQL

1. Introdução ao pgx

O pgx é um driver puramente Go para PostgreSQL que se destaca por ser implementado diretamente sobre o protocolo nativo do banco, sem depender de bibliotecas C externas como o libpq. Desenvolvido por Jack Christensen, o pacote github.com/jackc/pgx/v5 oferece acesso completo às funcionalidades específicas do PostgreSQL que o padrão database/sql não consegue expor.

A diferença fundamental entre pgx e database/sql está na arquitetura. Enquanto database/sql fornece uma abstração genérica que funciona com qualquer banco relacional, o pgx opera diretamente no protocolo PostgreSQL, eliminando camadas de abstração e ganhando desempenho. Isso significa que com pgx você tem acesso a tipos nativos como arrays, JSONB, UUID e intervalos sem necessidade de conversão manual.

A escolha entre pgx puro e database/sql depende do seu cenário. Se você precisa de máxima performance, tipos PostgreSQL avançados ou recursos como COPY e LISTEN/NOTIFY, opte pelo pgx puro. Se sua aplicação precisa ser portável entre bancos ou você está mantendo código legado, use pgx como driver para database/sql.

2. Configuração e Conexão

Para instalar o pgx:

go get github.com/jackc/pgx/v5

Criando uma conexão simples:

package main

import (
    "context"
    "fmt"
    "log"
    "github.com/jackc/pgx/v5"
)

func main() {
    conn, err := pgx.Connect(context.Background(), "postgres://usuario:senha@localhost:5432/meudb")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close(context.Background())

    fmt.Println("Conectado com sucesso!")
}

Para ambientes de produção, use pool de conexões:

import "github.com/jackc/pgx/v5/pgxpool"

pool, err := pgxpool.New(context.Background(), "postgres://usuario:senha@localhost:5432/meudb?sslmode=require&pool_max_conns=10")
if err != nil {
    log.Fatal(err)
}
defer pool.Close()

Configurações avançadas via URL de conexão:

// SSL/TLS, timeout e pool configurados na URL
connString := "postgres://usuario:senha@localhost:5432/meudb?" +
    "sslmode=verify-full&sslrootcert=/caminho/ca.pem&" +
    "connect_timeout=5&pool_max_conns=25&pool_min_conns=5"

3. Operações Básicas com pgx

Executando queries com diferentes abordagens:

// QueryRow - retorna uma única linha
var nome string
err := pool.QueryRow(ctx, "SELECT nome FROM usuarios WHERE id = $1", 1).Scan(&nome)

// Query - múltiplas linhas
rows, err := pool.Query(ctx, "SELECT id, nome, email FROM usuarios WHERE ativo = $1", true)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
    var usuario struct {
        ID    int
        Nome  string
        Email string
    }
    err := rows.Scan(&usuario.ID, &usuario.Nome, &usuario.Email)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Usuário: %+v\n", usuario)
}

// Exec - para INSERT, UPDATE, DELETE sem retorno de linhas
result, err := pool.Exec(ctx, 
    "UPDATE usuarios SET ultimo_acesso = NOW() WHERE id = $1", 42)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Linhas afetadas: %d\n", result.RowsAffected())

Tratamento de erros específicos do PostgreSQL:

import "github.com/jackc/pgx/v5/pgconn"

_, err := pool.Exec(ctx, "INSERT INTO usuarios (email) VALUES ($1)", "existente@email.com")
if err != nil {
    var pgErr *pgconn.PgError
    if errors.As(err, &pgErr) {
        switch pgErr.Code {
        case "23505": // unique_violation
            fmt.Println("Email já cadastrado")
        case "40001": // serialization_failure
            fmt.Println("Deadlock detectado, tente novamente")
        default:
            fmt.Printf("Erro PostgreSQL: %s\n", pgErr.Message)
        }
    }
}

4. Tipos Nativos do PostgreSQL e pgx

O pgx oferece suporte nativo a tipos complexos do PostgreSQL:

import "github.com/jackc/pgx/v5/pgtype"

type Produto struct {
    ID          int              `json:"id"`
    Nome        string           `json:"nome"`
    Tags        []string         `json:"tags"`         // array
    Metadados   map[string]any   `json:"metadados"`    // JSONB
    CodigoUUID  pgtype.UUID      `json:"codigo_uuid"`  // UUID
    Periodo     pgtype.Daterange `json:"periodo"`      // daterange
}

// Inserindo com tipos nativos
_, err := pool.Exec(ctx, `
    INSERT INTO produtos (nome, tags, metadados, codigo_uuid, periodo) 
    VALUES ($1, $2, $3, $4, $5)`,
    "Smartphone",
    []string{"eletrônicos", "promoção"},
    `{"cor": "preto", "garantia": 12}`,
    "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
    "[2024-01-01,2024-12-31)",
)

Trabalhando com enums customizados:

type StatusPedido string

const (
    StatusPendente StatusPedido = "pendente"
    StatusPago     StatusPedido = "pago"
    StatusEnviado  StatusPedido = "enviado"
)

// pgx mapeia strings para enums automaticamente
_, err := pool.Exec(ctx, 
    "INSERT INTO pedidos (status) VALUES ($1)", StatusPago)

5. Transações e Controle de Concorrência

Transações com pgx oferecem controle fino sobre isolamento:

tx, err := pool.Begin(ctx)
if err != nil {
    log.Fatal(err)
}
defer tx.Rollback(ctx) // rollback automático se não commitar

// Configurando nível de isolamento
_, err = tx.Exec(ctx, "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
if err != nil {
    log.Fatal(err)
}

// Operações dentro da transação
_, err = tx.Exec(ctx, "UPDATE contas SET saldo = saldo - $1 WHERE id = $2", 100, 1)
if err != nil {
    log.Fatal(err)
}

_, err = tx.Exec(ctx, "UPDATE contas SET saldo = saldo + $1 WHERE id = $2", 100, 2)
if err != nil {
    log.Fatal(err)
}

// Commit final
err = tx.Commit(ctx)
if err != nil {
    // Tratar serialization_failure para retry
    log.Fatal(err)
}

Retry automático para serialização:

func executarComRetry(ctx context.Context, pool *pgxpool.Pool, fn func(tx pgx.Tx) error) error {
    for tentativas := 0; tentativas < 3; tentativas++ {
        tx, err := pool.Begin(ctx)
        if err != nil {
            return err
        }

        err = fn(tx)
        if err != nil {
            tx.Rollback(ctx)

            var pgErr *pgconn.PgError
            if errors.As(err, &pgErr) && pgErr.Code == "40001" {
                continue // serialization_failure, tentar novamente
            }
            return err
        }

        return tx.Commit(ctx)
    }
    return fmt.Errorf("máximo de tentativas excedido")
}

6. Funcionalidades Avançadas do pgx

Inserção em massa com COPY protocol:

import "github.com/jackc/pgx/v5/pgxpool"

func inserirEmMassa(ctx context.Context, pool *pgxpool.Pool, usuarios []Usuario) error {
    copyFrom := pgx.CopyFromSlice(len(usuarios), func(i int) ([]any, error) {
        return []any{usuarios[i].Nome, usuarios[i].Email, usuarios[i].Ativo}, nil
    })

    _, err := pool.CopyFrom(ctx, 
        pgx.Identifier{"usuarios"}, 
        []string{"nome", "email", "ativo"}, 
        copyFrom,
    )
    return err
}

Notificações assíncronas com LISTEN/NOTIFY:

func ouvirNotificacoes(ctx context.Context, conn *pgx.Conn) {
    _, err := conn.Exec(ctx, "LISTEN canal_eventos")
    if err != nil {
        log.Fatal(err)
    }

    for {
        notification, err := conn.WaitForNotification(ctx)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("Notificação recebida: %s - %s\n", 
            notification.Channel, notification.Payload)
    }
}

Preparação de statements:

// Preparar statement para reutilização
_, err := pool.Prepare(ctx, "inserir_usuario", 
    "INSERT INTO usuarios (nome, email) VALUES ($1, $2) RETURNING id")
if err != nil {
    log.Fatal(err)
}

// Executar várias vezes
for i := 0; i < 100; i++ {
    var id int
    err := pool.QueryRow(ctx, "inserir_usuario", 
        fmt.Sprintf("user%d", i), fmt.Sprintf("user%d@email.com", i)).Scan(&id)
    if err != nil {
        log.Fatal(err)
    }
}

7. pgx com database/sql (Interface Padrão)

Usando pgx como driver para database/sql:

import (
    "database/sql"
    _ "github.com/jackc/pgx/v5/stdlib" // registro automático do driver
)

func main() {
    db, err := sql.Open("pgx", "postgres://usuario:senha@localhost:5432/meudb")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Agora usa a interface padrão database/sql
    rows, err := db.Query("SELECT id, nome FROM usuarios")
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var id int
        var nome string
        rows.Scan(&id, &nome)
        fmt.Printf("%d: %s\n", id, nome)
    }
}

Limitações dessa abordagem: você perde acesso a tipos nativos do PostgreSQL, COPY protocol, LISTEN/NOTIFY e preparação de statements. A vantagem é poder migrar gradualmente de outro driver para pgx sem refatorar todo o código.

8. Boas Práticas e Performance

Gerenciamento de pool otimizado:

config, err := pgxpool.ParseConfig("postgres://usuario:senha@localhost:5432/meudb")
if err != nil {
    log.Fatal(err)
}

config.MaxConns = 25
config.MinConns = 5
config.MaxConnLifetime = 30 * time.Minute
config.MaxConnIdleTime = 5 * time.Minute
config.HealthCheckPeriod = 1 * time.Minute

pool, err := pgxpool.NewWithConfig(ctx, config)

Monitoramento com QueryTracer:

type queryLogger struct{}

func (ql *queryLogger) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context {
    log.Printf("Query iniciada: %s | Args: %v", data.SQL, data.Args)
    return context.WithValue(ctx, "start", time.Now())
}

func (ql *queryLogger) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) {
    start := ctx.Value("start").(time.Time)
    duracao := time.Since(start)

    if duracao > 100*time.Millisecond {
        log.Printf("Query LENTA: %v | Duração: %v | Erro: %v", 
            data.SQL, duracao, data.Err)
    }
}

// Aplicar tracer
config.ConnConfig.Tracer = &queryLogger{}

Comparação de desempenho: em benchmarks reais, o pgx puro é 2x a 3x mais rápido que database/sql com pgx driver para operações intensivas, especialmente com COPY protocol e tipos complexos.


Referências