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+ driverpgxoupq— baixo nível, controle total, mais boilerplate.sqlx— açúcar em cima dodatabase/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:
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:
- Tabelas no plural (
users,orders) — convenção amplamente adotada. - Sempre tenha
id,created_at,updated_at— você vai querer no futuro, garantido. - Use
BIGSERIALouUUIDpara IDs —INTestoura mais cedo do que você imagina. - Foreign keys com
ON DELETEexplícito — defina o que acontece quando o pai some.
Exemplo:
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);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:
// 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:
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
LIMITem listagens. SemLIMIT, um dia a tabela cresce e seu sistema cai. - Use índices nas colunas que aparecem em
WHERE,JOINeORDER 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,gooseouatlas) — nunca altere o schema na mão em produção.