Go com PostgreSQL: conexão e CRUD usando pgx
Como conectar Go ao PostgreSQL? Instale github.com/jackc/pgx/v5, configure a variável DATABASE_URL e execute uma consulta com context.Context. Para uma API ou worker com requisições concorrentes, use pgxpool, compartilhando um pool por processo. Os mantenedores do pgx recomendam sua interface nativa quando a aplicação usa somente PostgreSQL e não precisa de bibliotecas que exigem database/sql.
Este tutorial de Go + PostgreSQL — também chamado de Golang com Postgres — começa com um programa executável e avança para CRUD com placeholders $1, pool, transações e migrations. O pgx é o driver; sqlc pode gerar código que usa esse driver, enquanto GORM oferece uma camada de ORM.
Se você já passou do primeiro QueryRow, consulte pgxpool em produção para métricas e configuração ou transações PostgreSQL em Go para locks, retry e isolamento.
O que você vai construir
| Etapa | Entrega |
|---|---|
| Setup | Postgres 16 no Docker + módulo Go com pgx |
| Conexão | pgxpool com ping, timeout e config de pool |
| CRUD | Create / Read / Update / Delete com RETURNING |
| Transações | Débito/crédito atômico com Begin + Rollback |
| Schema | Migrations versionadas com golang-migrate |
| Decisão | Quando ficar em pgx, ir para sqlc ou GORM |
Ao final você tem um repositório enxuto pronto para embutir numa API REST em Go e empacotar com Docker.
Setup: Postgres local e dependências
Docker para desenvolvimento
docker run --name postgres-go \
-e POSTGRES_USER=app \
-e POSTGRES_PASSWORD=senha123 \
-e POSTGRES_DB=myapp \
-p 127.0.0.1:5432:5432 \
-d postgres:16
O exemplo usa Postgres 16 e publica a porta somente em 127.0.0.1. A senha é fictícia e serve apenas para este banco local descartável; não a reutilize em produção. Aguarde o banco ficar pronto:
docker exec postgres-go pg_isready -U app -d myapp
Quando aparecer accepting connections, continue. Em Compose, veja a configuração ao lado da API no guia de Go com Docker.
Instalar pgx
mkdir go-postgres && cd go-postgres
go mod init example.com/go-postgres
go get github.com/jackc/pgx/v5
go get github.com/jackc/pgx/v5/pgxpool
Teste rápido de conexão
No mesmo terminal, defina a URL do banco local:
export DATABASE_URL='postgres://app:senha123@localhost:5432/myapp?sslmode=disable'
sslmode=disable é apenas para este ambiente local. Em produção, use a configuração TLS e a CA exigidas pelo provedor; verify-full verifica também o hostname do servidor (veja a documentação de TLS do PostgreSQL). Não publique arquivos .env com credenciais.
Salve o programa abaixo como main.go. Ele usa uma conexão única e um prazo de cinco segundos para conectar e consultar:
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/jackc/pgx/v5"
)
func main() {
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
panic("defina DATABASE_URL antes de executar")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
conn, err := pgx.Connect(ctx, dsn)
if err != nil {
panic(err)
}
defer func() {
closeCtx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
_ = conn.Close(closeCtx)
}()
var version string
if err := conn.QueryRow(ctx, "select version()").Scan(&version); err != nil {
panic(err)
}
fmt.Println(version)
}
Execute:
go run .
A saída deve incluir a versão do servidor, por exemplo PostgreSQL 16.... Se falhar, consulte a tabela abaixo antes de implementar o CRUD.
Erros comuns ao conectar Go ao Postgres
| Sintoma | O que verificar |
|---|---|
connection refused | Execute docker ps e pg_isready; confira se a porta local é 5432 e se o banco terminou de iniciar. |
password authentication failed | Confira usuário e senha da URL. Em volumes persistentes da imagem oficial, alterar POSTGRES_PASSWORD não redefine a senha de um banco já inicializado. |
database "myapp" does not exist | Confira POSTGRES_DB e o nome do banco no caminho da URL. |
| Erro de TLS/certificado | Use a CA e o hostname indicados pelo provedor. Não desative TLS para contornar um erro em produção. |
context deadline exceeded | Confira conectividade, carga do banco e prazo da operação; não aumente o timeout sem investigar. |
API em outro container? localhost aponta para o próprio container, não para o banco. Em Compose, use o nome do serviço (por exemplo postgres:5432) na URL. Se a senha tiver caracteres reservados como @, : ou /, aplique percent-encoding no componente da senha, conforme a sintaxe de URI do PostgreSQL.
pgx.Connect abre uma conexão única — útil para scripts, CLIs e testes de conexão. Para compartilhar conexões entre requisições, use pgxpool na próxima seção.
pgx ou database/sql? O pgx oferece tipos PostgreSQL, COPY, batch e cache de prepared statements. Quando uma biblioteca exige *sql.DB, use github.com/jackc/pgx/v5/stdlib. lib/pq é outro driver para database/sql; avalie os requisitos e a documentação atual do projeto antes de decidir sobre uma migração.
Conexão com pgxpool (caminho recomendado)
package main
import (
"context"
"log"
"os"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
log.Fatal("defina DATABASE_URL antes de executar")
}
cfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
log.Fatal(err)
}
cfg.MaxConns = 10
cfg.MinConns = 2
cfg.MaxConnLifetime = time.Hour
cfg.MaxConnIdleTime = 30 * time.Minute
cfg.HealthCheckPeriod = time.Minute
pool, err := pgxpool.NewWithConfig(ctx, cfg)
if err != nil {
log.Fatal(err)
}
defer pool.Close()
pingCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
if err := pool.Ping(pingCtx); err != nil {
log.Fatal(err)
}
log.Println("conectado ao PostgreSQL com pgxpool")
// ... handlers / workers usam o mesmo *pgxpool.Pool
}
Este bloco é uma alternativa ao main.go anterior, não deve ser colado como um segundo main. A criação do pool não garante que o banco esteja acessível: confirme com Ping. Os valores MaxConns=10 e MinConns=2 são exemplos, não uma recomendação universal. Planeje o total de conexões de todas as réplicas dentro do orçamento do banco, reservando espaço para administração e outros clientes.
Regras que evitam dor em produção:
- URL via env (
DATABASE_URL) — nunca hardcode senha. - Timeout em toda query —
context.WithTimeoutpor request HTTP ou job. - Um pool por processo — compartilhe o
*pgxpool.Pool; não abra conexão por request. - Feche no graceful shutdown — veja o padrão de context e timeout em Go e de middleware HTTP.
Detalhes de sizing, saturacão e métricas do pool estão em pgxpool para PostgreSQL em produção.
Modelo e schema inicial
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
Salve o SQL em schema.sql e aplique no banco local:
docker exec -i postgres-go psql -U app -d myapp < schema.sql
Em app real esse CREATE TABLE vira migration (seção abaixo), não Exec solto no boot — exceto protótipos. Os trechos de CRUD a seguir são funções para um repositório, não programas independentes. Além de User, declare var ErrNotFound = errors.New("usuário não encontrado") e importe context, errors, time, github.com/jackc/pgx/v5 e github.com/jackc/pgx/v5/pgxpool no arquivo que os reúne.
CRUD completo com pgx
Create
func createUser(ctx context.Context, pool *pgxpool.Pool, name, email string) (User, error) {
const q = `
INSERT INTO users (name, email)
VALUES ($1, $2)
RETURNING id, name, email, created_at`
var u User
err := pool.QueryRow(ctx, q, name, email).
Scan(&u.ID, &u.Name, &u.Email, &u.CreatedAt)
return u, err
}
RETURNING evita um SELECT extra e é o idioma natural do Postgres com Go.
Read (um e lista)
func getUserByID(ctx context.Context, pool *pgxpool.Pool, id int64) (User, error) {
const q = `SELECT id, name, email, created_at FROM users WHERE id = $1`
var u User
err := pool.QueryRow(ctx, q, id).
Scan(&u.ID, &u.Name, &u.Email, &u.CreatedAt)
if errors.Is(err, pgx.ErrNoRows) {
return User{}, ErrNotFound // erro de domínio, não 500
}
return u, err
}
func listUsers(ctx context.Context, pool *pgxpool.Pool) ([]User, error) {
const q = `SELECT id, name, email, created_at FROM users ORDER BY id`
rows, err := pool.Query(ctx, q)
if err != nil {
return nil, err
}
defer rows.Close()
var out []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.CreatedAt); err != nil {
return nil, err
}
out = append(out, u)
}
return out, rows.Err()
}
Sempre defer rows.Close() e cheque rows.Err() — vazar cursor esgota o pool.
Update e Delete
func updateUser(ctx context.Context, pool *pgxpool.Pool, id int64, name, email string) error {
tag, err := pool.Exec(ctx,
`UPDATE users SET name = $1, email = $2 WHERE id = $3`,
name, email, id,
)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return ErrNotFound
}
return nil
}
func deleteUser(ctx context.Context, pool *pgxpool.Pool, id int64) error {
tag, err := pool.Exec(ctx, `DELETE FROM users WHERE id = $1`, id)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return ErrNotFound
}
return nil
}
Trate “zero linhas” como 404 de domínio. Para o mapa de erros Go, veja tratamento de erros em Go.
Transações
func transfer(ctx context.Context, pool *pgxpool.Pool, fromID, toID int64, amount int64) error {
tx, err := pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx) // no-op depois do Commit
if amount <= 0 || fromID == toID {
return errors.New("transferência inválida")
}
tag, err := tx.Exec(ctx,
`UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1`,
amount, fromID,
)
if err != nil {
return err
}
if tag.RowsAffected() != 1 {
return errors.New("origem inexistente ou saldo insuficiente")
}
tag, err = tx.Exec(ctx,
`UPDATE accounts SET balance = balance + $1 WHERE id = $2`,
amount, toID,
)
if err != nil {
return err
}
if tag.RowsAffected() != 1 {
return errors.New("destino inexistente")
}
return tx.Commit(ctx)
}
O exemplo pressupõe uma tabela accounts com id único e balance inteiro não nulo (valor em unidades mínimas, como centavos). É uma demonstração de atomicidade, não um sistema financeiro completo: conferir RowsAffected impede creditar sem debitar ou perder o débito quando o destino não existe. Erros antes do commit provocam rollback; o contexto precisa continuar válido para o rollback.
Para SELECT … FOR UPDATE, deadlocks, retry em serialização e isolation levels, o guia dedicado é transações, locks e retry em Go.
Batch e prepared statements
Batch (vários inserts numa ida à rede)
batch := &pgx.Batch{}
batch.Queue(`INSERT INTO users (name, email) VALUES ($1, $2)`, "Alice", "[email protected]")
batch.Queue(`INSERT INTO users (name, email) VALUES ($1, $2)`, "Bob", "[email protected]")
br := pool.SendBatch(ctx, batch)
defer br.Close()
for i := 0; i < 2; i++ {
if _, err := br.Exec(); err != nil {
return err
}
}
Prepared statements
O pgx prepara statements automaticamente no nível da conexão do pool quando a mesma query se repete. Você raramente precisa chamar Prepare à mão em serviços HTTP; foque em SQL estável (sem montar o texto dinamicamente) para o cache de prepared statements funcionar.
Migrations com golang-migrate
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
migrate create -ext sql -dir migrations -seq create_users
# edite migrations/000001_create_users.up.sql e .down.sql
export DATABASE_URL='postgres://app:senha123@localhost:5432/myapp?sslmode=disable'
migrate -path migrations -database "$DATABASE_URL" up
Exemplo de par:
-- 000001_create_users.up.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 000001_create_users.down.sql
DROP TABLE IF EXISTS users;
Boas práticas:
- Rode
upno pipeline de deploy antes de subir pods novos. - Nunca edite uma migration já aplicada em produção — crie a próxima.
- Prefira SQL explícito a
AutoMigratede ORM em serviços críticos. - Guia irmão: migrations de banco em Go.
pgx vs database/sql vs sqlc vs GORM
| Abordagem | Quando usar | Custo |
|---|---|---|
| pgx direto | Controle total, hot path, features Postgres | Você escreve SQL e Scan |
| database/sql + pgx stdlib | Precisa de interface *sql.DB portável | Perde parte das APIs nativas do pgx |
| sqlc | SQL real + tipos gerados, zero reflexão | Workflow de geração de código |
| GORM | CRUD rápido, protótipo, admin interno | Abstração, N+1 se descuidar |
Escolha pelo projeto: pgx dá acesso direto às APIs do driver; sqlc gera código a partir de SQL e pode usar pgx; GORM oferece abstrações de ORM. Compare em profundidade em GORM em produção e sqlc typesafe.
Estrutura de projeto sugerida
project/
├── cmd/api/main.go
├── internal/
│ ├── db/pool.go # NewPool(cfg) *pgxpool.Pool
│ └── user/
│ ├── model.go
│ └── repository.go # Create/Get/List/Update/Delete
├── migrations/
│ ├── 000001_create_users.up.sql
│ └── 000001_create_users.down.sql
├── docker-compose.yml
└── go.mod
O repositório recebe *pgxpool.Pool (ou uma interface mínima) e devolve erros de domínio. Handlers HTTP ficam finos — encaixe natural com API REST em Go ou Chi em produção.
Testes de repositório
- Integração com Postgres de verdade (testcontainers ou Compose de CI) — preferível a mockar SQL.
- Rode migrations no
TestMainantes da suíte. - Cada teste abre uma transação e faz
ROLLBACKno cleanup, ou usa schema/database isolado. - Cubra
ErrNoRows, unique violation (23505) e timeout de context.
O kit geral de testes está em testes em Go; o padrão de tabela e t.Run se aplica igual a repositórios.
Checklist de produção
- Placeholders
$1…$n— zero concatenação de input. - Context com deadline em toda ida ao banco.
- Pool dimensionado por réplica; monitore
acquirewait. rows.Close()+rows.Err()sempre.- Migrations versionadas no deploy.
- Erros de domínio (
not found,conflict) separados de erros de infra. - TLS conforme o provedor — prefira
sslmode=verify-fullcom CA confiável para verificar o certificado e o hostname; não copiedisablepara produção. - Observabilidade — trace spans em volta de queries lentas; logs estruturados com slog.
- Secrets só em env/secret manager — nunca em imagem Docker.
- Graceful shutdown fecha o pool depois de drenar requests.
Armadilhas comuns
- Abrir
sql.Open/pgxpool.Newpor request — destrói latência e o Postgres. - Ignorar
pgx.ErrNoRows— vira 500 em vez de 404. - Pool gigante em cada pod — 20 pods × 50 conns = 1.000 sessões; o banco morre antes da app.
- Migration no boot da app com várias réplicas — corrida; rode no job de deploy.
- Scan de
NULLemstring/int64sem pointer/pgtype— use*stringou tipos do pgx. - Esquecer índice em
emailUNIQUE — o UNIQUE já cria índice, mas joins e filtros ad-hoc não.
Próximos passos na trilha
- Exponha o repositório numa API REST com validação e middleware.
- Ajuste o pool com o guia de pgxpool em produção.
- Adicione migrations formais e CI.
- Avalie sqlc se o volume de SQL crescer.
- Empacote API + Postgres com Docker Compose.
- Meça contenda com transações e locks.
Perguntas frequentes
Qual o melhor driver PostgreSQL para Go em 2026?
Para aplicações exclusivamente PostgreSQL, pgx/v5 é a recomendação deste guia, seguindo os critérios dos mantenedores: não depender de bibliotecas que exigem database/sql. Ele oferece pgxpool, tipos nativos, batch e COPY. Se a equipe precisa de *sql.DB, use o adaptador github.com/jackc/pgx/v5/stdlib. Não é preciso migrar um driver existente sem avaliar o benefício.
O que é go pgx / pgx em Golang?
pgx é um driver e toolkit PostgreSQL para Go, instalado como github.com/jackc/pgx/v5. pgx.Connect abre uma conexão; pgxpool gerencia conexões compartilhadas para APIs e workers. O pacote stdlib adapta o driver à interface database/sql.
Como conectar Go ao PostgreSQL com pgxpool?
go get github.com/jackc/pgx/v5/pgxpool, ParseConfig + NewWithConfig (ou New com a URL), Ping com timeout e um único pool compartilhado no processo. Configure MaxConns/MinConns e passe context em toda query.
Como conectar Golang ao PostgreSQL passo a passo?
go mod init, go get do pgx e do pgxpool, Postgres no Docker, pgxpool.New com a DATABASE_URL, Ping com timeout e queries com context + placeholders $1. As seções acima mostram cada passo com código completo — incluindo o teste rápido de conexão logo após a instalação.
Como fazer migrations em Go com PostgreSQL?
Com golang-migrate: arquivos .up.sql / .down.sql versionados, migrate up no deploy e down só em rollback controlado. Evite AutoMigrate de ORM como única estratégia em produção.
Quando usar GORM, sqlc ou pgx puro?
pgx para SQL explícito e APIs nativas do PostgreSQL; sqlc para gerar código Go tipado a partir de SQL; GORM quando a abstração de ORM for adequada ao projeto. sqlc pode gerar código para pgx, portanto não são alternativas mutuamente exclusivas.
Como evitar SQL injection e vazamento de conexão?
Placeholders sempre; nunca fmt.Sprintf na query. defer rows.Close(), context com timeout, pool único e tratamento correto de ErrNoRows.
Qual tamanho de connection pool usar?
Não há tamanho universal. Planeje o orçamento usando max_connections do banco, quantidade de réplicas e outros clientes; reserve conexões administrativas. MaxConns=10 no exemplo demonstra a API. Ajuste com testes de carga, espera por aquisição e carga no PostgreSQL, não apenas pelo número de CPUs.
Referências oficiais
- pgx: documentação e critérios para escolher a interface nativa — recursos do driver e uso com
database/sql. - pgxpool: API de configuração, Ping e estatísticas — comportamento do pool e limites.
- PostgreSQL: strings de conexão e TLS — URL, percent-encoding e modos SSL.
- Imagem oficial PostgreSQL no Docker — variáveis de inicialização e comportamento com volumes existentes.
- golang-migrate: tutorial PostgreSQL — criação e execução de migrations.
Veja também
- pgxpool em Go para PostgreSQL em produção — sizing, métricas e falhas do pool
- sqlc + PostgreSQL typesafe — SQL real com código gerado
- GORM em produção — ORM, relações e comparação com sqlc
- Migrations de banco em Go — pipeline de schema
- Transações, locks e retry — isolamento e contenção
- API REST com Go — exponha o repositório via HTTP
- Go com Docker — Compose com API + Postgres
- Testes em Go — integração de repositório e race detector
- Tratamento de erros em Go —
ErrNoRowse erros de domínio - Go para backend — mapa da stack backend
- Vagas Go no Brasil — onde essa skill aparece em job descriptions