Go

O que Todo Dev Deve Saber sobre PostgreSQL com Go: pgx Driver, Transactions e Connection Pool

16 min de leitura

O que Todo Dev Deve Saber sobre PostgreSQL com Go: pgx Driver, Transactions e Connection Pool

Introdução ao pgx: Por que ele é a melhor escolha para Go 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 , o pgx foi construído especificamente para PostgreSQL, aproveitando seus recursos nativos como tipos customizados, prepared statements eficientes e melhor tratamento de erros. O pgx oferece dois níveis de abstração: a interface 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. Instalação e Configuração Inicial Instalando o pgx Comece adicionando o pgx ao seu projeto Go: Se você também precisar de suporte , instale o driver: Estabelecendo sua primeira conexão A forma

<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 (

&quot;context&quot;

&quot;fmt&quot;

&quot;log&quot;

&quot;github.com/jackc/pgx/v5&quot;

)

func main() {

// URL de conexão PostgreSQL

dbURL := &quot;postgres://usuario:senha@localhost:5432/meu_banco&quot;

// 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(&quot;Erro ao conectar: %v&quot;, err)

}

defer conn.Close(ctx)

// Testa a conexão

var greeting string

err = conn.QueryRow(ctx, &quot;select &#039;PostgreSQL com pgx funcionando!&#039;&quot;).Scan(&amp;greeting)

if err != nil {

log.Fatalf(&quot;Erro na query: %v&quot;, 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 (

&quot;context&quot;

&quot;fmt&quot;

&quot;log&quot;

&quot;time&quot;

&quot;github.com/jackc/pgx/v5/pgxpool&quot;

)

func main() {

ctx := context.Background()

// Configuração manual do pool

config, err := pgxpool.ParseConfig(&quot;postgres://usuario:senha@localhost:5432/meu_banco&quot;)

if err != nil {

log.Fatalf(&quot;Erro ao fazer parse da config: %v&quot;, 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(&quot;Erro ao criar pool: %v&quot;, err)

}

defer pool.Close()

// Verificar a conexão

err = pool.Ping(ctx)

if err != nil {

log.Fatalf(&quot;Ping falhou: %v&quot;, err)

}

fmt.Println(&quot;Pool de conexões criado e verificado com sucesso!&quot;)

// Usar o pool em uma operação

var name string

err = pool.QueryRow(ctx, &quot;SELECT &#039;Olá do pool&#039;::text&quot;).Scan(&amp;name)

if err != nil {

log.Fatalf(&quot;Erro: %v&quot;, 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 &quot;espera por conexão&quot;.</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 (

&quot;context&quot;

&quot;fmt&quot;

&quot;log&quot;

&quot;time&quot;

&quot;github.com/jackc/pgx/v5/pgxpool&quot;

)

// 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(&quot;falha ao iniciar transação: %w&quot;, 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,

&quot;UPDATE contas SET saldo = saldo - $1 WHERE id = $2 RETURNING saldo&quot;,

valor, contaOrigem).Scan(&amp;saldoOrigem)

if err != nil {

return fmt.Errorf(&quot;falha ao debitar: %w&quot;, err)

}

if saldoOrigem &lt; 0 {

return fmt.Errorf(&quot;saldo insuficiente&quot;)

}

// Segunda operação: adicionar à conta destino

_, err = tx.Exec(ctx,

&quot;UPDATE contas SET saldo = saldo + $1 WHERE id = $2&quot;,

valor, contaDestino)

if err != nil {

return fmt.Errorf(&quot;falha ao creditar: %w&quot;, err)

}

// Registrar a transação

_, err = tx.Exec(ctx,

&quot;INSERT INTO historico (conta_origem, conta_destino, valor, data) VALUES ($1, $2, $3, $4)&quot;,

contaOrigem, contaDestino, valor, time.Now())

if err != nil {

return fmt.Errorf(&quot;falha ao registrar: %w&quot;, err)

}

// Se chegou aqui, tudo correu bem. Fazer commit.

err = tx.Commit(ctx)

if err != nil {

return fmt.Errorf(&quot;falha ao fazer commit: %w&quot;, err)

}

return nil

}

func main() {

config, _ := pgxpool.ParseConfig(&quot;postgres://usuario:senha@localhost:5432/meu_banco&quot;)

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(&quot;Transferência falhou: %v&quot;, err)

} else {

fmt.Println(&quot;Transferência concluída com sucesso!&quot;)

}

}</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, &quot;INSERT INTO logs (mensagem) VALUES (&#039;Etapa 1&#039;)&quot;)

// Criar um savepoint (ponto de restauração)

_, _ = tx.Exec(ctx, &quot;SAVEPOINT sp1&quot;)

// Segunda parte — se falhar, volta só até o savepoint

err := tx.QueryRow(ctx, &quot;SELECT 1 WHERE FALSE&quot;).Scan()

if err != nil {

// Rollback apenas até o savepoint, não até o início

tx.Exec(ctx, &quot;ROLLBACK TO sp1&quot;)

// Continuar a transação é possível

_, _ = tx.Exec(ctx, &quot;INSERT INTO logs (mensagem) VALUES (&#039;Recuperado do erro&#039;)&quot;)

}

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 (

&quot;context&quot;

&quot;log&quot;

&quot;github.com/jackc/pgx/v5/pgxpool&quot;

)

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,

&quot;SELECT nome FROM usuarios WHERE cidade = $1 ORDER BY nome&quot;,

cidade)

if err != nil {

return nil, err

}

defer rows.Close()

var nomes []string

for rows.Next() {

var nome string

if err := rows.Scan(&amp;nome); err != nil {

return nil, err

}

nomes = append(nomes, nome)

}

return nomes, rows.Err()

}

func main() {

config, _ := pgxpool.ParseConfig(&quot;postgres://usuario:senha@localhost:5432/meu_banco&quot;)

pool, _ := pgxpool.NewWithConfig(context.Background(), config)

defer pool.Close()

nomes, err := buscarUsuariosPorCidade(pool, &quot;São Paulo&quot;)

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 := &amp;pgx.Batch{}

// Adicionar múltiplas queries ao batch

for _, d := range dados {

batch.Queue(

&quot;INSERT INTO pessoas (nome, email, idade) VALUES ($1, $2, $3)&quot;,

d[&quot;nome&quot;], d[&quot;email&quot;], d[&quot;idade&quot;])

}

// Executar tudo de uma vez

results := pool.SendBatch(ctx, batch)

defer results.Close()

// Verificar resultados

for i := 0; i &lt; 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 &quot;github.com/jackc/pgx/v5/pgtype&quot;

type Usuario struct {

ID int db:&quot;id&quot;

Nome string db:&quot;nome&quot;

Email string db:&quot;email&quot;

}

func buscarUsuarios(pool *pgxpool.Pool) ([]Usuario, error) {

ctx := context.Background()

rows, _ := pool.Query(ctx, &quot;SELECT id, nome, email FROM usuarios&quot;)

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 (

&quot;github.com/jackc/pgx/v5&quot;

&quot;github.com/jackc/pgx/v5/pgconn&quot;

)

func operacaoRobusta(pool *pgxpool.Pool) error {

ctx := context.Background()

_, err := pool.Exec(ctx, &quot;INSERT INTO usuarios (email) VALUES ($1)&quot;, &quot;teste@example.com&quot;)

if err == nil {

return nil

}

// Verificar se é um erro específico do PostgreSQL

var pgErr *pgconn.PgError

if errors.As(err, &amp;pgErr) {

switch pgErr.Code {

case &quot;23505&quot;: // unique_violation

return fmt.Errorf(&quot;email já existe no banco&quot;)

case &quot;23502&quot;: // not_null_violation

return fmt.Errorf(&quot;campo obrigatório vazio&quot;)

case &quot;23503&quot;: // foreign_key_violation

return fmt.Errorf(&quot;referência inválida&quot;)

default:

return fmt.Errorf(&quot;erro do banco: %s&quot;, pgErr.Message)

}

}

// Verificar se a conexão foi perdida (transitório)

if errors.Is(err, pgx.ErrNoRows) {

return fmt.Errorf(&quot;nenhum resultado encontrado&quot;)

}

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 &quot;time&quot;

func operacaoComRetry(pool *pgxpool.Pool, maxTentativas int) error {

var lastErr error

for tentativa := 0; tentativa &lt; 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, &amp;pgErr) &amp;&amp; !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(&quot;operação falhou após %d tentativas: %w&quot;, maxTentativas, lastErr)

}

func isTransient(code string) bool {

// Código &quot;08&quot; é server-specific, &quot;09&quot; é triggered action exception, etc.

return code == &quot;40001&quot; || code == &quot;40P01&quot; // 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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Guia Completo de Gin em Go: Framework Web de Alta Performance na Prática
Guia Completo de Gin em Go: Framework Web de Alta Performance na Prática

O que é Gin e Por Que Escolher Este Framework Gin é um framework web escrito...

Como Usar database/sql em Go: Conexão, Queries e Boas Práticas Nativas em Produção
Como Usar database/sql em Go: Conexão, Queries e Boas Práticas Nativas em Produção

Fundamentos do Package database/sql O package é a abstração padrão da linguag...

O que Todo Dev Deve Saber sobre Documentação de APIs Go com Swagger e swaggo
O que Todo Dev Deve Saber sobre Documentação de APIs Go com Swagger e swaggo

Entendendo APIs e a Importância da Documentação Uma API (Application Programm...