<h2>Introdução ao pgx: Por que ele é a melhor escolha para Go</h2>
<p>Quando você começar a trabalhar com PostgreSQL em Go, rapidamente descobrirá que existem várias bibliotecas disponíveis. O pgx é amplamente considerado a melhor opção da comunidade, e não é por acaso. Diferentemente do driver puro <code>database/sql</code>, o pgx foi construído especificamente para PostgreSQL, aproveitando seus recursos nativos como tipos customizados, prepared statements eficientes e melhor tratamento de erros.</p>
<p>O pgx oferece dois níveis de abstração: a interface <code>database/sql</code> padrão (compatibilidade) e sua própria API de baixo nível, que é mais expressiva e performática. Para aplicações sérias, você usará principalmente a API do pgx diretamente. A escolha é sua, mas neste artigo focaremos na abordagem direta com pgx, que é o padrão da indústria para sistemas de alta performance.</p>
<h2>Instalação e Configuração Inicial</h2>
<h3>Instalando o pgx</h3>
<p>Comece adicionando o pgx ao seu projeto Go:</p>
<pre><code class="language-bash">go get github.com/jackc/pgx/v5</code></pre>
<p>Se você também precisar de suporte <code>database/sql</code>, instale o driver:</p>
<pre><code class="language-bash">go get github.com/jackc/pgx/v5/stdlib</code></pre>
<h3>Estabelecendo sua primeira conexão</h3>
<p>A forma correta de trabalhar com bancos de dados é sempre usar um context. O pgx foi desenhado com esta filosofia desde o início. Aqui está um exemplo funcional:</p>
<pre><code class="language-go">package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5"
)
func main() {
// URL de conexão PostgreSQL
dbURL := "postgres://usuario:senha@localhost:5432/meu_banco"
// context com timeout para evitar travamentos
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Conecta ao banco
conn, err := pgx.Connect(ctx, dbURL)
if err != nil {
log.Fatalf("Erro ao conectar: %v", err)
}
defer conn.Close(ctx)
// Testa a conexão
var greeting string
err = conn.QueryRow(ctx, "select 'PostgreSQL com pgx funcionando!'").Scan(&greeting)
if err != nil {
log.Fatalf("Erro na query: %v", err)
}
fmt.Println(greeting)
}</code></pre>
<p>A chave aqui é entender que <strong>tudo em pgx é context-aware</strong>. Você passa um context em cada operação, permitindo que a aplicação tenha controle fino sobre timeouts e cancelamento. Não force a barra — sempre use contexts corretamente.</p>
<h2>Connection Pool e Gerenciamento de Recursos</h2>
<h3>O papel crítico do pool de conexões</h3>
<p>Uma conexão TCP é um recurso caro. Criar uma nova conexão para cada requisição destruiria a performance de qualquer aplicação. Por isso, usamos um pool — reutilizamos conexões existentes quando disponíveis. O pgx fornece <code>pgxpool</code>, que é exatamente isso: um pool robusto e thread-safe de conexões.</p>
<h3>Configurando um pool profissional</h3>
<pre><code class="language-go">package main
import (
"context"
"fmt"
"log"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
// Configuração manual do pool
config, err := pgxpool.ParseConfig("postgres://usuario:senha@localhost:5432/meu_banco")
if err != nil {
log.Fatalf("Erro ao fazer parse da config: %v", err)
}
// Ajustes de performance
config.MaxConns = 25 // máximo de conexões ativas
config.MinConns = 5 // mínimo mantido aquecido
config.MaxConnLifetime = 5 * time.Minute // reconectar a cada 5 minutos
config.MaxConnIdleTime = 2 * time.Minute // fechar se inativa por 2 min
config.HealthCheckPeriod = 30 * time.Second // verificar saúde a cada 30s
// Criar o pool
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
log.Fatalf("Erro ao criar pool: %v", err)
}
defer pool.Close()
// Verificar a conexão
err = pool.Ping(ctx)
if err != nil {
log.Fatalf("Ping falhou: %v", err)
}
fmt.Println("Pool de conexões criado e verificado com sucesso!")
// Usar o pool em uma operação
var name string
err = pool.QueryRow(ctx, "SELECT 'Olá do pool'::text").Scan(&name)
if err != nil {
log.Fatalf("Erro: %v", err)
}
fmt.Println(name)
}</code></pre>
<p>Os valores que você define aqui são críticos. Se <code>MaxConns</code> é muito baixo, sua aplicação ficará enfileirada esperando conexões. Se é muito alto, você sobrecarregará o servidor PostgreSQL. Para a maioria das aplicações web com Go, 20-30 é um bom ponto de partida. Ajuste conforme monitorar a métrica de "espera por conexão".</p>
<h2>Transações: O Coração da Integridade de Dados</h2>
<h3>Entendendo transações ACID</h3>
<p>Uma transação é um conjunto de operações que ou todas completam com sucesso, ou nenhuma toma efeito. Isto garante a consistência do seus dados. No mundo PostgreSQL com pgx, transações são a forma correta de fazer múltiplas operações relacionadas.</p>
<h3>Implementando transações corretamente</h3>
<pre><code class="language-go">package main
import (
"context"
"fmt"
"log"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
// Função que demonstra uma transação real
func transferirSaldo(pool *pgxpool.Pool, contaOrigem, contaDestino int64, valor float64) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Iniciar transação explicitamente
tx, err := pool.Begin(ctx)
if err != nil {
return fmt.Errorf("falha ao iniciar transação: %w", err)
}
// IMPORTANTE: usar defer para rollback em caso de erro
defer tx.Rollback(ctx)
// Primeira operação: deduzir da conta origem
var saldoOrigem float64
err = tx.QueryRow(ctx,
"UPDATE contas SET saldo = saldo - $1 WHERE id = $2 RETURNING saldo",
valor, contaOrigem).Scan(&saldoOrigem)
if err != nil {
return fmt.Errorf("falha ao debitar: %w", err)
}
if saldoOrigem < 0 {
return fmt.Errorf("saldo insuficiente")
}
// Segunda operação: adicionar à conta destino
_, err = tx.Exec(ctx,
"UPDATE contas SET saldo = saldo + $1 WHERE id = $2",
valor, contaDestino)
if err != nil {
return fmt.Errorf("falha ao creditar: %w", err)
}
// Registrar a transação
_, err = tx.Exec(ctx,
"INSERT INTO historico (conta_origem, conta_destino, valor, data) VALUES ($1, $2, $3, $4)",
contaOrigem, contaDestino, valor, time.Now())
if err != nil {
return fmt.Errorf("falha ao registrar: %w", err)
}
// Se chegou aqui, tudo correu bem. Fazer commit.
err = tx.Commit(ctx)
if err != nil {
return fmt.Errorf("falha ao fazer commit: %w", err)
}
return nil
}
func main() {
config, _ := pgxpool.ParseConfig("postgres://usuario:senha@localhost:5432/meu_banco")
pool, _ := pgxpool.NewWithConfig(context.Background(), config)
defer pool.Close()
// Executar a transferência
err := transferirSaldo(pool, 1, 2, 100.50)
if err != nil {
log.Printf("Transferência falhou: %v", err)
} else {
fmt.Println("Transferência concluída com sucesso!")
}
}</code></pre>
<p>Note a estrutura: você inicia a transação, executa operações, e <strong>sempre</strong> faz defer do rollback antes de qualquer coisa. Se tudo correr bem, você faz commit explicitamente, o que cancela o rollback deferred. Se qualquer erro ocorrer, o defer do rollback garante que nada foi alterado no banco. Isto é uma pattern que você verá em todo código Go profissional.</p>
<h3>Níveis de isolamento e configurações avançadas</h3>
<pre><code class="language-go">// Se você precisar de mais controle, use TxOptions
func operacaoComIsolamento(pool *pgxpool.Pool) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Usar SERIALIZABLE para máxima segurança (mais lento)
txOptions := pgx.TxOptions{
Isolation: pgx.Serializable,
AccessMode: pgx.ReadWrite,
}
tx, err := pool.BeginTx(ctx, txOptions)
if err != nil {
return err
}
defer tx.Rollback(ctx)
// ... suas operações aqui ...
return tx.Commit(ctx)
}</code></pre>
<p>Os níveis de isolamento (Read Uncommitted, Read Committed, Repeatable Read, Serializable) oferecem diferentes graus de segurança contra problemas de concorrência. Para 99% dos casos, o padrão Read Committed é adequado. Use Serializable apenas se realmente precisar — ele é significativamente mais lento.</p>
<h3>Savepoints: divisão dentro de transações</h3>
<pre><code class="language-go">func operacaoComSavepoint(pool *pgxpool.Pool) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
tx, _ := pool.Begin(ctx)
defer tx.Rollback(ctx)
// Primeira parte da operação
_, _ = tx.Exec(ctx, "INSERT INTO logs (mensagem) VALUES ('Etapa 1')")
// Criar um savepoint (ponto de restauração)
_, _ = tx.Exec(ctx, "SAVEPOINT sp1")
// Segunda parte — se falhar, volta só até o savepoint
err := tx.QueryRow(ctx, "SELECT 1 WHERE FALSE").Scan()
if err != nil {
// Rollback apenas até o savepoint, não até o início
tx.Exec(ctx, "ROLLBACK TO sp1")
// Continuar a transação é possível
_, _ = tx.Exec(ctx, "INSERT INTO logs (mensagem) VALUES ('Recuperado do erro')")
}
return tx.Commit(ctx)
}</code></pre>
<p>Savepoints são úteis em operações complexas onde você quer poder recuperar de erro parcial sem abandonar toda a transação. Contudo, na maioria dos casos, estruturar seu código para falhas atômicas (tudo ou nada) é mais limpo.</p>
<h2>Queries Eficientes, Prepared Statements e Batch Operations</h2>
<h3>Por que prepared statements importam</h3>
<p>Prepared statements compilam a query uma vez no servidor e reutilizam o plano de execução para múltiplas execuções com parâmetros diferentes. Isto melhora performance e, mais importante, protege contra SQL injection.</p>
<pre><code class="language-go">package main
import (
"context"
"log"
"github.com/jackc/pgx/v5/pgxpool"
)
func buscarUsuariosPorCidade(pool *pgxpool.Pool, cidade string) ([]string, error) {
ctx := context.Background()
// pgx usa prepared statements automaticamente internamente
// Você não precisa fazer nada especial — basta usar placeholders $1, $2, etc.
rows, err := pool.Query(ctx,
"SELECT nome FROM usuarios WHERE cidade = $1 ORDER BY nome",
cidade)
if err != nil {
return nil, err
}
defer rows.Close()
var nomes []string
for rows.Next() {
var nome string
if err := rows.Scan(&nome); err != nil {
return nil, err
}
nomes = append(nomes, nome)
}
return nomes, rows.Err()
}
func main() {
config, _ := pgxpool.ParseConfig("postgres://usuario:senha@localhost:5432/meu_banco")
pool, _ := pgxpool.NewWithConfig(context.Background(), config)
defer pool.Close()
nomes, err := buscarUsuariosPorCidade(pool, "São Paulo")
if err != nil {
log.Fatal(err)
}
for _, nome := range nomes {
println(nome)
}
}</code></pre>
<h3>Operações em batch para performance extrema</h3>
<p>Quando você precisa inserir ou atualizar milhares de registros, fazer uma query por vez é loucura. pgx oferece <code>Batch</code>, que agrupa múltiplas queries e as envia em uma única viagem de rede:</p>
<pre><code class="language-go">func inserirMuitosRegistros(pool *pgxpool.Pool, dados []map[string]interface{}) error {
ctx := context.Background()
// Criar um batch
batch := &pgx.Batch{}
// Adicionar múltiplas queries ao batch
for _, d := range dados {
batch.Queue(
"INSERT INTO pessoas (nome, email, idade) VALUES ($1, $2, $3)",
d["nome"], d["email"], d["idade"])
}
// Executar tudo de uma vez
results := pool.SendBatch(ctx, batch)
defer results.Close()
// Verificar resultados
for i := 0; i < len(dados); i++ {
_, err := results.Exec()
if err != nil {
return err
}
}
return nil
}</code></pre>
<p>Para um cenário com 10.000 inserts, batch operations são <strong>ordens de magnitude</strong> mais rápidas que loop com queries individuais.</p>
<h3>Scanning eficiente com RowToStructTag</h3>
<pre><code class="language-go">import "github.com/jackc/pgx/v5/pgtype"
type Usuario struct {
ID int db:"id"
Nome string db:"nome"
Email string db:"email"
}
func buscarUsuarios(pool *pgxpool.Pool) ([]Usuario, error) {
ctx := context.Background()
rows, _ := pool.Query(ctx, "SELECT id, nome, email FROM usuarios")
defer rows.Close()
// pgx.CollectRows automáticamente mapeia colunas para campos da struct
usuarios, err := pgx.CollectRows(rows, pgx.RowToStructByName[Usuario])
if err != nil {
return nil, err
}
return usuarios, nil
}</code></pre>
<p>Isto elimina o tedioso trabalho de fazer <code>.Scan()</code> manualmente para cada campo.</p>
<h2>Tratamento de Erros e Resiliência</h2>
<h3>Diferenciando tipos de erro</h3>
<p>Nem todo erro é igual. Alguns são transitórios (rede caiu momentaneamente), outros são permanentes (violação de constraint):</p>
<pre><code class="language-go">import (
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
func operacaoRobusta(pool *pgxpool.Pool) error {
ctx := context.Background()
_, err := pool.Exec(ctx, "INSERT INTO usuarios (email) VALUES ($1)", "teste@example.com")
if err == nil {
return nil
}
// Verificar se é um erro específico do PostgreSQL
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
switch pgErr.Code {
case "23505": // unique_violation
return fmt.Errorf("email já existe no banco")
case "23502": // not_null_violation
return fmt.Errorf("campo obrigatório vazio")
case "23503": // foreign_key_violation
return fmt.Errorf("referência inválida")
default:
return fmt.Errorf("erro do banco: %s", pgErr.Message)
}
}
// Verificar se a conexão foi perdida (transitório)
if errors.Is(err, pgx.ErrNoRows) {
return fmt.Errorf("nenhum resultado encontrado")
}
return err
}</code></pre>
<p>Conhecer os códigos de erro PostgreSQL é essencial para tratamento profissional.</p>
<h3>Retry com backoff exponencial</h3>
<pre><code class="language-go">import "time"
func operacaoComRetry(pool *pgxpool.Pool, maxTentativas int) error {
var lastErr error
for tentativa := 0; tentativa < maxTentativas; tentativa++ {
err := operacaoQuePodefalhar(pool)
if err == nil {
return nil
}
lastErr = err
// Não fazer retry para erros permanentes
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && !isTransient(pgErr.Code) {
return err
}
// Esperar com backoff exponencial: 1s, 2s, 4s, etc.
espera := time.Duration(math.Pow(2, float64(tentativa))) * time.Second
time.Sleep(espera)
}
return fmt.Errorf("operação falhou após %d tentativas: %w", maxTentativas, lastErr)
}
func isTransient(code string) bool {
// Código "08" é server-specific, "09" é triggered action exception, etc.
return code == "40001" || code == "40P01" // serialization_failure, deadlock_detected
}</code></pre>
<h2>Conclusão</h2>
<p>Você aprendeu três conceitos fundamentais que separam Go profissional de código de hobby:</p>
<ol>
<li><strong>Connection pools não são opcionais</strong> — use <code>pgxpool</code> sempre. Conexões são caras; reutilizá-las é a diferença entre uma aplicação que aguenta 100 requisições/segundo e uma que aguenta 10.000.</li>
</ol>
<ol>
<li><strong>Transações garantem integridade</strong> — a pattern defer-rollback é sagrada. Se você não entendeu por que sempre fazer <code>defer tx.Rollback(ctx)</code> antes de <code>tx.Commit(ctx)</code>, releia a seção de transações até ficar claro.</li>
</ol>
<ol>
<li><strong>Detalhes importam</strong> — contexts com timeouts, prepared statements automáticos, batch operations, tratamento específico de erros PostgreSQL. Cada um destes detalhes é a diferença entre código que passa em testes pequenos e código que funciona em produção com 10 milhões de requisições por dia.</li>
</ol>
<h2>Referências</h2>
<ul>
<li><a href="https://pkg.go.dev/github.com/jackc/pgx/v5" target="_blank" rel="noopener noreferrer">pgx Documentation Official</a></li>
<li><a href="https://www.postgresql.org/docs/current/errcodes-appendix.html" target="_blank" rel="noopener noreferrer">PostgreSQL Error Codes</a></li>
<li><a href="https://www.postgresql.org/docs/current/tutorial-transactions.html" target="_blank" rel="noopener noreferrer">PostgreSQL Documentation - Transactions</a></li>
<li><a href="https://golang.org/doc/database/sql-best-practices" target="_blank" rel="noopener noreferrer">Go Database/SQL Best Practices</a></li>
<li><a href="https://github.com/jackc/pgx/wiki" target="_blank" rel="noopener noreferrer">pgx Tutorial - GitHub Repository</a></li>
</ul>
<p><!-- FIM --></p>