Go com PostgreSQL: Tutorial Completo
Resposta rápida: em 2026, conecte Go ao PostgreSQL com pgx/v5 + pgxpool, não com lib/pq. Monte o pool a partir de DATABASE_URL, rode queries com context.Context e placeholders $1, versionize o schema com golang-migrate e isole o acesso em repositórios. O CRUD abaixo compila; o restante da página mostra pool, transações, batch, testes e quando subir para sqlc ou GORM.
PostgreSQL é o banco padrão de quase todo backend Go no Brasil — fintechs, marketplaces e SaaS. Este guia é o mapa de integração Go + PostgreSQL do zero ao básico de produção: driver certo, CRUD, pool, migrations, transações e armadilhas. Se você já passou do primeiro QueryRow e precisa afinar o pool, leia o guia de pgxpool em produção. Para locks, retry e isolamento, veja transações PostgreSQL em Go.
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 5432:5432 \
-d postgres:16
Use Postgres 16 (ou a versão da sua nuvem). Em Compose, o mesmo serviço costuma aparecer ao lado da API no guia de Go com Docker.
Instalar pgx
go mod init example.com/go-postgres
go get github.com/jackc/pgx/v5
go get github.com/jackc/pgx/v5/pgxpool
Por que não lib/pq? O driver clássico github.com/lib/pq está em manutenção e não recebe features novas. pgx é o padrão de fato da comunidade Go: mais rápido no protocolo binário, pool próprio, suporte a tipos nativos do Postgres (uuid, jsonb, arrays), COPY, batch e prepared statements. Só use database/sql + stdlib do pgx se precisar de uma interface genérica entre bancos.
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 == "" {
dsn = "postgres://app:senha123@localhost:5432/myapp?sslmode=disable"
}
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
}
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"`
}
Em app real esse CREATE TABLE vira migration (seção abaixo), não Exec solto no boot — exceto protótipos.
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 _, err := tx.Exec(ctx,
`UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1`,
amount, fromID,
); err != nil {
return err
}
if _, err := tx.Exec(ctx,
`UPDATE accounts SET balance = balance + $1 WHERE id = $2`,
amount, toID,
); err != nil {
return err
}
return tx.Commit(ctx)
}
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 |
Regra prática no mercado BR: API de produto costuma ficar em pgx ou sqlc; painéis e CRUDs internos às vezes usam GORM. 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. - SSL (
sslmode=require) fora de localhost. - 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?
pgx/v5. É o padrão da comunidade: protocolo binário rápido, pgxpool, tipos nativos, batch e COPY. lib/pq só faz sentido em código legado. Se a equipe exige database/sql, use o adapter stdlib do próprio pgx.
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 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 controle e hot path; sqlc para SQL explícito com tipos gerados; GORM para CRUD rápido onde a abstração paga o custo. Muitos times misturam: sqlc/pgx no core, GORM em módulos internos.
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?
Comece com 4–10 conexões por pod, meça fila de acquire e carga no Postgres, e suba com parcimônia. Multiplique mentalmente por número de réplicas antes de copiar um MaxOpenConns=100 de tutorial antigo.
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