Janeiro 2026 · ~10 min

Go com PostgreSQL: Tutorial Completo

Conecte Go ao PostgreSQL com pgx/v5: CRUD, pgxpool, migrations, transações e context. Código de produção e quando usar GORM ou sqlc no Brasil em 2026.

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

EtapaEntrega
SetupPostgres 16 no Docker + módulo Go com pgx
Conexãopgxpool com ping, timeout e config de pool
CRUDCreate / Read / Update / Delete com RETURNING
TransaçõesDébito/crédito atômico com Begin + Rollback
SchemaMigrations versionadas com golang-migrate
DecisãoQuando 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:

  1. URL via env (DATABASE_URL) — nunca hardcode senha.
  2. Timeout em toda querycontext.WithTimeout por request HTTP ou job.
  3. Um pool por processo — compartilhe o *pgxpool.Pool; não abra conexão por request.
  4. 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 up no 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 AutoMigrate de ORM em serviços críticos.
  • Guia irmão: migrations de banco em Go.

pgx vs database/sql vs sqlc vs GORM

AbordagemQuando usarCusto
pgx diretoControle total, hot path, features PostgresVocê escreve SQL e Scan
database/sql + pgx stdlibPrecisa de interface *sql.DB portávelPerde parte das APIs nativas do pgx
sqlcSQL real + tipos gerados, zero reflexãoWorkflow de geração de código
GORMCRUD rápido, protótipo, admin internoAbstraçã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

  1. Integração com Postgres de verdade (testcontainers ou Compose de CI) — preferível a mockar SQL.
  2. Rode migrations no TestMain antes da suíte.
  3. Cada teste abre uma transação e faz ROLLBACK no cleanup, ou usa schema/database isolado.
  4. 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

  1. Placeholders $1…$n — zero concatenação de input.
  2. Context com deadline em toda ida ao banco.
  3. Pool dimensionado por réplica; monitore acquire wait.
  4. rows.Close() + rows.Err() sempre.
  5. Migrations versionadas no deploy.
  6. Erros de domínio (not found, conflict) separados de erros de infra.
  7. SSL (sslmode=require) fora de localhost.
  8. Observabilidade — trace spans em volta de queries lentas; logs estruturados com slog.
  9. Secrets só em env/secret manager — nunca em imagem Docker.
  10. Graceful shutdown fecha o pool depois de drenar requests.

Armadilhas comuns

  • Abrir sql.Open/pgxpool.New por 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 NULL em string/int64 sem pointer/pgtype — use *string ou tipos do pgx.
  • Esquecer índice em email UNIQUE — o UNIQUE já cria índice, mas joins e filtros ad-hoc não.

Próximos passos na trilha

  1. Exponha o repositório numa API REST com validação e middleware.
  2. Ajuste o pool com o guia de pgxpool em produção.
  3. Adicione migrations formais e CI.
  4. Avalie sqlc se o volume de SQL crescer.
  5. Empacote API + Postgres com Docker Compose.
  6. 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

Perguntas frequentes

Qual o melhor driver PostgreSQL para Go em 2026?

O pgx (github.com/jackc/pgx/v5) é o driver recomendado. É mais rápido que lib/pq (em manutenção), tem pool nativo com pgxpool, tipos PostgreSQL, COPY, batch e prepared statements. Use database/sql + pgxstd só quando precisar de uma API genérica entre bancos.

Como conectar Go ao PostgreSQL com pgxpool?

Instale github.com/jackc/pgx/v5/pgxpool, chame pgxpool.New(ctx, databaseURL), faça Ping e configure MaxConns, MinConns e MaxConnLifetime via ParseConfig. Sempre passe context.Context com timeout nas queries e feche o pool no shutdown.

Como fazer migrations em Go com PostgreSQL?

Use golang-migrate (CLI ou biblioteca): crie pares .up.sql/.down.sql versionados, rode migrate -path ./migrations -database $DATABASE_URL up em deploy e down em rollback. Em produção, prefira migrations SQL explícitas a AutoMigrate de ORM.

Quando usar GORM, sqlc ou pgx puro?

pgx puro (ou database/sql) para controle total e SQL explícito; sqlc quando quiser SQL real com tipos gerados e zero reflexão; GORM quando produtividade de CRUD e relacionamentos simples superam o custo de abstração. Em serviços de alta carga, a maioria dos times Go no Brasil combina pgx/sqlc + repositórios.

Como evitar SQL injection e vazamento de conexão em Go?

Use sempre placeholders ($1, $2) — nunca concatene entrada do usuário na query. Feche rows com defer, use context com timeout, configure o pool e trate sql.ErrNoRows / pgx.ErrNoRows como 'não encontrado', não como falha de infraestrutura.

Qual tamanho de connection pool usar com Go e PostgreSQL?

Comece pequeno: 4–10 conexões por réplica de app em Kubernetes costuma bastar. Ajuste MaxConns pela fórmula aproximada (núcleos do Postgres × 2) dividida pelo número de pods, meça wait time e idle. Pool grande demais satura o Postgres; pool pequeno demais enfileira requests.