M02·04Banco de dados

CAPÍTULO 04

Banco de dados

Integração com PostgreSQL usando pgx, modelagem sólida, queries seguras e boas práticas que evitam dor de cabeça.

Por Thiago Souza12 min de leituraAtualizado em 2026-05

Integração com PostgreSQL

Analogia da estante de livros: o banco é uma estante gigante e organizada. Cada tabela é uma prateleira. Cada linha é um livro. O SQL é o "pedido" que você faz para a bibliotecária ("me traz todos os livros do Tolkien publicados depois de 1950").

Em Go, as opções mais comuns são:

  • database/sql + driver pgx ou pq — baixo nível, controle total, mais boilerplate.
  • sqlx — açúcar em cima do database/sql (scan direto em struct).
  • sqlc — gera código Go a partir de SQL puro. Excelente para projetos sérios.
  • GORM — ORM completo. Cômodo, mas pode esconder problemas de performance.

Para a maioria dos backends profissionais, recomendo pgx direto ou sqlc. ORM é tentador, mas em Go a comunidade tende a preferir SQL explícito.

Conexão básica com pgx:

go
package postgres
 
import (
    "context"
    "github.com/jackc/pgx/v5/pgxpool"
)
 
// Connect cria um pool de conexões. Pool = conjunto de conexões reutilizáveis.
// Por que pool? Abrir conexão custa caro. Pool mantém algumas abertas, prontas pra uso.
func Connect(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
    cfg, err := pgxpool.ParseConfig(dsn)
    if err != nil {
        return nil, err
    }
 
    // configurações importantes em produção
    cfg.MaxConns = 25                  // teto de conexões simultâneas
    cfg.MinConns = 5                   // mínimo mantido aberto
    cfg.MaxConnLifetime = time.Hour    // recicla conexões velhas
    cfg.MaxConnIdleTime = 30 * time.Minute
 
    pool, err := pgxpool.NewWithConfig(ctx, cfg)
    if err != nil {
        return nil, err
    }
 
    // testa se a conexão realmente funciona
    if err := pool.Ping(ctx); err != nil {
        return nil, err
    }
 
    return pool, nil
}

Modelagem básica

Quatro princípios que evitam dor de cabeça:

  1. Tabelas no plural (users, orders) — convenção amplamente adotada.
  2. Sempre tenha id, created_at, updated_at — você vai querer no futuro, garantido.
  3. Use BIGSERIAL ou UUID para IDs — INT estoura mais cedo do que você imagina.
  4. Foreign keys com ON DELETE explícito — defina o que acontece quando o pai some.

Exemplo:

sql
CREATE TABLE users (
    id           BIGSERIAL PRIMARY KEY,
    email        VARCHAR(255) NOT NULL UNIQUE,    -- UNIQUE evita duplicata
    password_hash TEXT NOT NULL,
    created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
 
CREATE TABLE orders (
    id          BIGSERIAL PRIMARY KEY,
    user_id     BIGINT NOT NULL,
    total_cents BIGINT NOT NULL,                  -- dinheiro em centavos! NUNCA float
    status      VARCHAR(20) NOT NULL DEFAULT 'pending',
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
 
    -- se o usuário for deletado, deleta os pedidos junto
    CONSTRAINT fk_user FOREIGN KEY (user_id)
        REFERENCES users(id) ON DELETE CASCADE
);
 
-- índice em coluna que será filtrada com frequência
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status  ON orders(status);
Dinheiro nunca em FLOAT. Use BIGINT em centavos ou NUMERIC(15,2). Float dá imprecisão e isso vira processo na justiça.

Queries e boas práticas

Sempre use parâmetros, NUNCA concatenação de string — isso é o santo graal contra SQL injection:

go
// HORROR: SQL injection garantido
query := "SELECT * FROM users WHERE email = '" + email + "'"
 
// Correto: o driver escapa para você
query := "SELECT * FROM users WHERE email = $1"
row := db.QueryRow(ctx, query, email)

Repositório completo de exemplo:

go
package postgres
 
import (
    "context"
    "errors"
    "github.com/jackc/pgx/v5"
    "github.com/jackc/pgx/v5/pgxpool"
    "meuprojeto/internal/user"
)
 
type UserRepo struct {
    db *pgxpool.Pool
}
 
func NewUserRepo(db *pgxpool.Pool) *UserRepo {
    return &UserRepo{db: db}
}
 
// Save insere um novo usuário e devolve com ID preenchido.
func (r *UserRepo) Save(ctx context.Context, u user.User) (user.User, error) {
    // RETURNING traz colunas geradas pelo banco (ID, timestamps).
    query := `
        INSERT INTO users (email, password_hash)
        VALUES ($1, $2)
        RETURNING id, created_at, updated_at
    `
    err := r.db.QueryRow(ctx, query, u.Email, u.PasswordHash).
        Scan(&u.ID, &u.CreatedAt, &u.UpdatedAt)
 
    if err != nil {
        return user.User{}, fmt.Errorf("salvar usuário: %w", err)
    }
    return u, nil
}
 
// FindByEmail traduz "não encontrado" do driver para o erro de domínio.
// Isso é importantíssimo: a camada de cima não pode depender de pgx.
func (r *UserRepo) FindByEmail(ctx context.Context, email string) (user.User, error) {
    var u user.User
    query := `SELECT id, email, password_hash, created_at, updated_at
              FROM users WHERE email = $1`
 
    err := r.db.QueryRow(ctx, query, email).Scan(
        &u.ID, &u.Email, &u.PasswordHash, &u.CreatedAt, &u.UpdatedAt,
    )
 
    if errors.Is(err, pgx.ErrNoRows) {
        return user.User{}, user.ErrNotFound  // erro do nosso domínio
    }
    if err != nil {
        return user.User{}, fmt.Errorf("buscar usuário: %w", err)
    }
    return u, nil
}

Boas práticas resumidas:

  • Sempre LIMIT em listagens. Sem LIMIT, um dia a tabela cresce e seu sistema cai.
  • Use índices nas colunas que aparecem em WHERE, JOIN e ORDER BY.
  • Transações para operações que precisam ser atômicas (mover dinheiro, criar X + Y juntos).
  • EXPLAIN ANALYZE é seu melhor amigo quando uma query estiver lenta.
  • Migrations versionadas (com golang-migrate, goose ou atlas) — nunca altere o schema na mão em produção.