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
- Documentação oficial do pgx v5 — Repositório oficial com documentação completa, exemplos e guia de migração da v4 para v5
- pgxpool package documentation — Documentação detalhada do pacote de pool de conexões, incluindo configurações avançadas
- PostgreSQL COPY protocol with pgx — Documentação oficial do PostgreSQL sobre o protocolo COPY, com exemplos de uso com pgx
- Tutorial: Working with PostgreSQL in Go using pgx — Tutorial prático cobrindo desde conexão básica até funcionalidades avançadas como transações e tipos nativos
- Benchmark: pgx vs database/sql performance — Discussão técnica no GitHub comparando desempenho entre pgx puro e database/sql com diversos drivers PostgreSQL