PostgreSQL é um dos bancos de dados relacionais mais robustos e populares do mundo. Combinado com Go, você tem uma stack poderosa para aplicações de alta performance. Neste guia, você aprenderá todas as técnicas essenciais para trabalhar com PostgreSQL em Go, desde operações básicas até padrões avançados de produção.
Por Que PostgreSQL com Go?
O Match Perfeito
┌─────────────────────────────────────────────────────────────────┐
│ POSTGRESQL │ GO │
├─────────────────────────────────────────────────────────────────┤
│ Confiável │ Type-safe, compilação rigorosa │
├─────────────────────────────────────────────────────────────────┤
│ ACID transactions │ Error handling explícito │
├─────────────────────────────────────────────────────────────────┤
│ Alto desempenho │ Runtime eficiente, baixa latência │
├─────────────────────────────────────────────────────────────────┤
│ Extensível │ Interfaces flexíveis │
├─────────────────────────────────────────────────────────────────┤
│ Open source │ Open source │
└─────────────────────────────────────────────────────────────────┘
Configuração do Projeto
Instalação das Dependências
# Driver padrão PostgreSQL
go get -u github.com/lib/pq
# Driver alternativo (mais performático)
go get -u github.com/jackc/pgx/v5
# sqlc - type-safe SQL
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
# Migration tool
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
Estrutura do Projeto
project/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── config/
│ │ └── database.go
│ ├── models/
│ │ └── user.go
│ ├── repository/
│ │ └── user_repository.go
│ └── service/
│ └── user_service.go
├── migrations/
│ ├── 001_create_users_table.up.sql
│ ├── 001_create_users_table.down.sql
│ └── ...
├── queries/
│ └── user.sql
├── sqlc.yaml
├── go.mod
└── .env
Conexão com PostgreSQL
Usando database/sql (Padrão)
// internal/config/database.go
package config
import (
"database/sql"
"fmt"
"log"
"os"
"time"
_ "github.com/lib/pq"
)
// Configurações do banco
type DBConfig struct {
Host string
Port int
User string
Password string
Database string
SSLMode string
}
func NewDBConfig() *DBConfig {
return &DBConfig{
Host: getEnv("DB_HOST", "localhost"),
Port: getEnvAsInt("DB_PORT", 5432),
User: getEnv("DB_USER", "postgres"),
Password: getEnv("DB_PASSWORD", "password"),
Database: getEnv("DB_NAME", "myapp"),
SSLMode: getEnv("DB_SSLMODE", "disable"),
}
}
func (c *DBConfig) ConnectionString() string {
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
c.Host, c.Port, c.User, c.Password, c.Database, c.SSLMode)
}
// Conexão com pool
func NewDB(cfg *DBConfig) (*sql.DB, error) {
db, err := sql.Open("postgres", cfg.ConnectionString())
if err != nil {
return nil, fmt.Errorf("falha ao abrir conexão: %w", err)
}
// Configurar pool de conexões
db.SetMaxOpenConns(25) // Máximo de conexões abertas
db.SetMaxIdleConns(10) // Máximo de conexões idle
db.SetConnMaxLifetime(time.Hour) // Tempo máximo de vida
db.SetConnMaxIdleTime(30 * time.Minute)
// Verificar conexão
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
return nil, fmt.Errorf("falha ao ping banco de dados: %w", err)
}
log.Println("✅ Conectado ao PostgreSQL")
return db, nil
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvAsInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
var intValue int
fmt.Sscanf(value, "%d", &intValue)
return intValue
}
return defaultValue
}
Usando pgx (Recomendado)
// internal/config/pgx_database.go
package config
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5/pgxpool"
)
func NewPgxPool(cfg *DBConfig) (*pgxpool.Pool, error) {
connString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s&pool_max_conns=25",
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database, cfg.SSLMode)
pool, err := pgxpool.New(context.Background(), connString)
if err != nil {
return nil, fmt.Errorf("falha ao criar pool: %w", err)
}
// Verificar conexão
if err := pool.Ping(context.Background()); err != nil {
return nil, fmt.Errorf("falha ao ping: %w", err)
}
log.Println("✅ Conectado ao PostgreSQL com pgx")
return pool, nil
}
Modelos e Estruturas
// internal/models/user.go
package models
import (
"time"
)
// User representa um usuário no banco
type User struct {
ID int64 `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Password string `json:"-"` // Não serializar
Active bool `json:"active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// UserCreate é usado para criar usuários
type UserCreate struct {
Email string `json:"email" validate:"required,email"`
Name string `json:"name" validate:"required,min=3,max=100"`
Password string `json:"password" validate:"required,min=8"`
}
// UserUpdate é usado para atualizar usuários
type UserUpdate struct {
Email *string `json:"email,omitempty"`
Name *string `json:"name,omitempty"`
Active *bool `json:"active,omitempty"`
}
// TableName retorna o nome da tabela
func (User) TableName() string {
return "users"
}
Operações CRUD
Repository Pattern
// internal/repository/user_repository.go
package repository
import (
"context"
"database/sql"
"errors"
"fmt"
"project/internal/models"
)
var (
ErrUserNotFound = errors.New("usuário não encontrado")
ErrEmailExists = errors.New("email já existe")
)
// UserRepository define operações de banco
type UserRepository interface {
Create(ctx context.Context, user *models.UserCreate) (*models.User, error)
GetByID(ctx context.Context, id int64) (*models.User, error)
GetByEmail(ctx context.Context, email string) (*models.User, error)
List(ctx context.Context, limit, offset int) ([]*models.User, error)
Update(ctx context.Context, id int64, update *models.UserUpdate) (*models.User, error)
Delete(ctx context.Context, id int64) error
Count(ctx context.Context) (int64, error)
}
// userRepo implementa UserRepository
type userRepo struct {
db *sql.DB
}
// NewUserRepository cria nova instância
func NewUserRepository(db *sql.DB) UserRepository {
return &userRepo{db: db}
}
CREATE
// Create insere novo usuário
func (r *userRepo) Create(ctx context.Context, user *models.UserCreate) (*models.User, error) {
query := `
INSERT INTO users (email, name, password, active, created_at, updated_at)
VALUES ($1, $2, $3, $4, NOW(), NOW())
RETURNING id, email, name, active, created_at, updated_at
`
// Em produção, faça hash da senha!
hashedPassword := hashPassword(user.Password)
var created models.User
err := r.db.QueryRowContext(ctx, query,
user.Email,
user.Name,
hashedPassword,
true,
).Scan(
&created.ID,
&created.Email,
&created.Name,
&created.Active,
&created.CreatedAt,
&created.UpdatedAt,
)
if err != nil {
// Verificar erro de duplicação
if isDuplicateKeyError(err) {
return nil, ErrEmailExists
}
return nil, fmt.Errorf("falha ao criar usuário: %w", err)
}
return &created, nil
}
func hashPassword(password string) string {
// Use bcrypt em produção!
// import "golang.org/x/crypto/bcrypt"
// hashed, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
// return string(hashed)
return "hashed_" + password
}
func isDuplicateKeyError(err error) bool {
if err == nil {
return false
}
// Verificar código de erro PostgreSQL para duplicação
return strings.Contains(err.Error(), "duplicate key")
}
READ
// GetByID busca usuário por ID
func (r *userRepo) GetByID(ctx context.Context, id int64) (*models.User, error) {
query := `
SELECT id, email, name, active, created_at, updated_at
FROM users
WHERE id = $1
`
var user models.User
err := r.db.QueryRowContext(ctx, query, id).Scan(
&user.ID,
&user.Email,
&user.Name,
&user.Active,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound
}
return nil, fmt.Errorf("falha ao buscar usuário: %w", err)
}
return &user, nil
}
// GetByEmail busca usuário por email
func (r *userRepo) GetByEmail(ctx context.Context, email string) (*models.User, error) {
query := `
SELECT id, email, name, active, created_at, updated_at
FROM users
WHERE email = $1
`
var user models.User
err := r.db.QueryRowContext(ctx, query, email).Scan(
&user.ID,
&user.Email,
&user.Name,
&user.Active,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound
}
return nil, fmt.Errorf("falha ao buscar usuário: %w", err)
}
return &user, nil
}
// List retorna lista paginada de usuários
func (r *userRepo) List(ctx context.Context, limit, offset int) ([]*models.User, error) {
query := `
SELECT id, email, name, active, created_at, updated_at
FROM users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`
if limit <= 0 {
limit = 10
}
if limit > 100 {
limit = 100 // Máximo por página
}
rows, err := r.db.QueryContext(ctx, query, limit, offset)
if err != nil {
return nil, fmt.Errorf("falha ao listar usuários: %w", err)
}
defer rows.Close()
var users []*models.User
for rows.Next() {
var user models.User
err := rows.Scan(
&user.ID,
&user.Email,
&user.Name,
&user.Active,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("falha ao scan usuário: %w", err)
}
users = append(users, &user)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("erro ao iterar rows: %w", err)
}
return users, nil
}
// Count retorna total de usuários
func (r *userRepo) Count(ctx context.Context) (int64, error) {
query := `SELECT COUNT(*) FROM users`
var count int64
err := r.db.QueryRowContext(ctx, query).Scan(&count)
if err != nil {
return 0, fmt.Errorf("falha ao contar usuários: %w", err)
}
return count, nil
}
UPDATE
// Update atualiza usuário
func (r *userRepo) Update(ctx context.Context, id int64, update *models.UserUpdate) (*models.User, error) {
// Construir query dinamicamente
var setParts []string
var args []interface{}
argIndex := 1
if update.Email != nil {
setParts = append(setParts, fmt.Sprintf("email = $%d", argIndex))
args = append(args, *update.Email)
argIndex++
}
if update.Name != nil {
setParts = append(setParts, fmt.Sprintf("name = $%d", argIndex))
args = append(args, *update.Name)
argIndex++
}
if update.Active != nil {
setParts = append(setParts, fmt.Sprintf("active = $%d", argIndex))
args = append(args, *update.Active)
argIndex++
}
if len(setParts) == 0 {
return nil, errors.New("nenhum campo para atualizar")
}
// Sempre atualizar updated_at
setParts = append(setParts, fmt.Sprintf("updated_at = $%d", argIndex))
args = append(args, time.Now())
argIndex++
// Adicionar ID
args = append(args, id)
query := fmt.Sprintf(`
UPDATE users
SET %s
WHERE id = $%d
RETURNING id, email, name, active, created_at, updated_at
`, strings.Join(setParts, ", "), argIndex)
var user models.User
err := r.db.QueryRowContext(ctx, query, args...).Scan(
&user.ID,
&user.Email,
&user.Name,
&user.Active,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound
}
if isDuplicateKeyError(err) {
return nil, ErrEmailExists
}
return nil, fmt.Errorf("falha ao atualizar usuário: %w", err)
}
return &user, nil
}
DELETE
// Delete remove usuário
func (r *userRepo) Delete(ctx context.Context, id int64) error {
query := `DELETE FROM users WHERE id = $1`
result, err := r.db.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("falha ao deletar usuário: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("falha ao obter rows afetadas: %w", err)
}
if rowsAffected == 0 {
return ErrUserNotFound
}
return nil
}
// SoftDelete (alternativa ao delete físico)
func (r *userRepo) SoftDelete(ctx context.Context, id int64) error {
query := `
UPDATE users
SET active = false, deleted_at = NOW()
WHERE id = $1
`
result, err := r.db.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("falha ao deletar usuário: %w", err)
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
return ErrUserNotFound
}
return nil
}
Transações
// WithTransaction executa função dentro de transação
func (r *userRepo) WithTransaction(ctx context.Context, fn func(*sql.Tx) error) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("falha ao iniciar transação: %w", err)
}
defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p)
}
}()
if err := fn(tx); err != nil {
tx.Rollback()
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("falha ao commit: %w", err)
}
return nil
}
// TransferBalance exemplo de transação
func TransferBalance(ctx context.Context, db *sql.DB, fromID, toID int64, amount float64) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// Verificar saldo
var balance float64
err = tx.QueryRowContext(ctx,
"SELECT balance FROM accounts WHERE id = $1 FOR UPDATE",
fromID,
).Scan(&balance)
if err != nil {
return err
}
if balance < amount {
return errors.New("saldo insuficiente")
}
// Debitar
_, err = tx.ExecContext(ctx,
"UPDATE accounts SET balance = balance - $1 WHERE id = $2",
amount, fromID,
)
if err != nil {
return err
}
// Creditar
_, err = tx.ExecContext(ctx,
"UPDATE accounts SET balance = balance + $1 WHERE id = $2",
amount, toID,
)
if err != nil {
return err
}
// Registrar transação
_, err = tx.ExecContext(ctx,
`INSERT INTO transactions (from_id, to_id, amount, created_at)
VALUES ($1, $2, $3, NOW())`,
fromID, toID, amount,
)
if err != nil {
return err
}
return tx.Commit()
}
sqlc: Type-Safe SQL
Configuração
# sqlc.yaml
version: "2"
sql:
- schema: "migrations/"
queries: "queries/"
engine: "postgresql"
gen:
go:
package: "db"
out: "internal/db"
sql_package: "pgx/v5"
emit_json_tags: true
emit_prepared_queries: true
emit_interface: true
emit_empty_slices: true
Queries SQL
-- queries/user.sql
-- name: CreateUser :one
INSERT INTO users (email, name, password, active)
VALUES ($1, $2, $3, $4)
RETURNING *;
-- name: GetUser :one
SELECT * FROM users
WHERE id = $1;
-- name: GetUserByEmail :one
SELECT * FROM users
WHERE email = $1;
-- name: ListUsers :many
SELECT * FROM users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2;
-- name: UpdateUser :one
UPDATE users
SET email = COALESCE($2, email),
name = COALESCE($3, name),
active = COALESCE($4, active),
updated_at = NOW()
WHERE id = $1
RETURNING *;
-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;
-- name: CountUsers :one
SELECT COUNT(*) FROM users;
Gerar Código
# Gerar código Go a partir das queries
sqlc generate
# Estrutura gerada:
# internal/db/
# ├── db.go
# ├── models.go
# ├── querier.go (interface)
# └── user.sql.go
Uso do sqlc
// internal/repository/user_sqlc.go
package repository
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
"project/internal/db"
)
type UserSQLCRepository struct {
queries *db.Queries
}
func NewUserSQLCRepository(queries *db.Queries) *UserSQLCRepository {
return &UserSQLCRepository{queries: queries}
}
func (r *UserSQLCRepository) Create(ctx context.Context, email, name, password string) (*db.User, error) {
user, err := r.queries.CreateUser(ctx, db.CreateUserParams{
Email: email,
Name: name,
Password: password,
Active: true,
})
if err != nil {
return nil, err
}
return &user, nil
}
func (r *UserSQLCRepository) List(ctx context.Context, limit, offset int32) ([]db.User, error) {
return r.queries.ListUsers(ctx, db.ListUsersParams{
Limit: limit,
Offset: offset,
})
}
func (r *UserSQLCRepository) Update(ctx context.Context, id int64, email, name *string, active *bool) (*db.User, error) {
var emailParam, nameParam pgtype.Text
var activeParam pgtype.Bool
if email != nil {
emailParam = pgtype.Text{String: *email, Valid: true}
}
if name != nil {
nameParam = pgtype.Text{String: *name, Valid: true}
}
if active != nil {
activeParam = pgtype.Bool{Bool: *active, Valid: true}
}
user, err := r.queries.UpdateUser(ctx, db.UpdateUserParams{
ID: id,
Email: emailParam,
Name: nameParam,
Active: activeParam,
})
if err != nil {
return nil, err
}
return &user, nil
}
Migrations
Usando golang-migrate
# Criar nova migration
migrate create -ext sql -dir migrations -seq create_users_table
# Aplicar migrations
migrate -path migrations -database "postgres://user:pass@localhost/db?sslmode=disable" up
# Reverter última migration
migrate -path migrations -database "postgres://user:pass@localhost/db?sslmode=disable" down 1
Migration Files
-- migrations/001_create_users_table.up.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
password VARCHAR(255) NOT NULL,
active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_active ON users(active);
-- Trigger para atualizar updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- migrations/001_create_users_table.down.sql
DROP TRIGGER IF EXISTS update_users_updated_at ON users;
DROP FUNCTION IF EXISTS update_updated_at_column();
DROP INDEX IF EXISTS idx_users_active;
DROP INDEX IF EXISTS idx_users_email;
DROP TABLE IF EXISTS users;
Boas Práticas
1. Context para Timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
user, err := repo.GetByID(ctx, id)
2. Prepared Statements
// database/sql já usa prepared statements internamente
// Para queries repetidas, use Prepare
stmt, err := db.PrepareContext(ctx, "SELECT * FROM users WHERE id = $1")
if err != nil {
return err
}
defer stmt.Close()
for _, id := range ids {
row := stmt.QueryRowContext(ctx, id)
// ...
}
3. Connection Pool
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(time.Hour)
4. Error Handling
var (
ErrNotFound = errors.New("recurso não encontrado")
ErrDuplicate = errors.New("recurso já existe")
ErrConstraint = errors.New("violação de constraint")
)
func mapError(err error) error {
if err == nil {
return nil
}
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}
pqErr, ok := err.(*pq.Error)
if ok {
switch pqErr.Code {
case "23505": // unique_violation
return ErrDuplicate
case "23503": // foreign_key_violation
return ErrConstraint
}
}
return err
}
5. Logging
// Log queries em desenvolvimento
func logQuery(query string, args ...interface{}) {
if os.Getenv("ENV") == "development" {
log.Printf("[SQL] %s | Args: %v", query, args)
}
}
Checklist para Produção
- Connection pool configurado corretamente
- Timeouts em todas as queries (context)
- Migrations automatizadas no deploy
- Prepared statements para queries frequentes
- Índices otimizados
- Hash de senhas (bcrypt/argon2)
- Soft delete para dados importantes
- Auditoria (created_at, updated_at)
- SSL para conexões (sslmode=require)
- Backup automatizado
Próximos Passos
Aprofunde seus conhecimentos:
- Go CLI Tools - Ferramentas de linha de comando
- Go e gRPC - APIs de alta performance
- Go para Microserviços - Arquitetura distribuída
- Go Observability - Logs e métricas
Go + PostgreSQL: performance e confiabilidade em produção. Compartilhe seu projeto!