Migrations com golang-migrate
1. Introdução ao golang-migrate
Migrations são uma forma controlada e versionada de gerenciar alterações no esquema do banco de dados. Em projetos Go, onde a simplicidade e a previsibilidade são valorizadas, o uso de migrations traz benefícios como rastreabilidade, reprodutibilidade e facilidade de deploy.
O golang-migrate (v4) é uma biblioteca leve e independente que oferece suporte a múltiplos bancos de dados (PostgreSQL, MySQL, SQLite, etc.) e fontes de arquivos (sistema de arquivos, embed, S3, GitHub). Diferente de soluções acopladas a ORMs como GORM ou Goose, o golang-migrate funciona de forma desacoplada, permitindo que você use qualquer driver SQL nativo.
Comparado a outras ferramentas:
- GORM AutoMigrate: prático para desenvolvimento, mas perigoso em produção por não versionar alterações destrutivas.
- Goose: similar em filosofia, mas com menos drivers e fontes disponíveis.
- sql-migrate: depende de arquivos YAML e é menos flexível para uso programático.
O golang-migrate se destaca por sua API limpa, CLI nativa e ampla gama de adaptadores.
2. Instalação e Configuração Inicial
Para instalar a CLI do migrate:
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
A tag postgres compila com suporte ao driver PostgreSQL. Para outros bancos, use mysql, sqlite, etc.
A estrutura de diretórios recomendada:
migrations/
├── 000001_create_users_table.up.sql
├── 000001_create_users_table.down.sql
├── 000002_add_email_unique.up.sql
└── 000002_add_email_unique.down.sql
Cada migration é composta por um par de arquivos: {versão}_{descricao}.up.sql e {versão}_{descricao}.down.sql.
3. Criando Migrations: UP e DOWN
Para criar uma nova migration via CLI:
migrate create -ext sql -dir migrations -seq create_users_table
Isso gera dois arquivos. O .up.sql contém as alterações a aplicar:
-- 000001_create_users_table.up.sql
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
O .down.sql contém o rollback:
-- 000001_create_users_table.down.sql
DROP TABLE IF EXISTS users;
Boas práticas:
- Use IF NOT EXISTS/IF EXISTS para tornar as migrations idempotentes.
- Mantenha cada migration atômica (uma única alteração lógica).
- Versionamento sequencial (000001, 000002...).
- Nunca edite uma migration já aplicada em produção — crie uma nova.
4. Conectando ao Banco de Dados com pgx
O golang-migrate suporta nativamente o driver pgx (recomendado para PostgreSQL). A string de conexão segue o formato padrão:
package main
import (
"database/sql"
"fmt"
"log"
"os"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/pgx"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
func main() {
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
dbURL = "postgres://user:password@localhost:5432/mydb?sslmode=disable"
}
db, err := sql.Open("pgx", dbURL)
if err != nil {
log.Fatal("Erro ao conectar:", err)
}
defer db.Close()
driver, err := pgx.WithInstance(db, &pgx.Config{})
if err != nil {
log.Fatal("Erro ao criar driver:", err)
}
fmt.Println("Conexão estabelecida com sucesso")
}
Use variáveis de ambiente para evitar expor credenciais no código.
5. Executando Migrations em Código
A execução programática oferece controle total sobre o processo:
package main
import (
"log"
"os"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/pgx"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
func main() {
migrationsPath := "file://migrations"
dbURL := os.Getenv("DATABASE_URL")
m, err := migrate.New(migrationsPath, dbURL)
if err != nil {
log.Fatal("Erro ao criar migrate:", err)
}
defer m.Close()
// Aplica todas as migrations pendentes
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
log.Fatal("Erro no Up:", err)
}
// Controle granular com Steps
// m.Steps(2) // Sobe 2 migrations
// m.Steps(-1) // Desce 1 migration
// Rollback completo
// m.Down()
// Forçar versão em caso de dirty state
// m.Force(1)
}
Para verificar o estado atual:
version, dirty, err := m.Version()
if err != nil {
log.Fatal(err)
}
log.Printf("Versão atual: %d, Dirty: %v", version, dirty)
6. Migrations em Pipeline CI/CD
Em ambientes de staging e produção, a execução deve ser segura e controlada.
Estratégia recomendada:
- Container de inicialização: Use um init container no Kubernetes ou script de pré-deploy que execute as migrations.
- Lock de banco: O golang-migrate usa locks nativos do banco (ex:
pg_advisory_lockno PostgreSQL) para evitar concorrência. - Rollback automatizado: Em caso de falha, registre o erro e pare o deploy.
Exemplo de script para deploy:
package main
import (
"log"
"os"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/pgx"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
func runMigrations() error {
m, err := migrate.New("file://migrations", os.Getenv("DATABASE_URL"))
if err != nil {
return err
}
defer m.Close()
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
return err
}
return nil
}
func main() {
if err := runMigrations(); err != nil {
log.Fatal("Falha nas migrations:", err)
}
log.Println("Migrations aplicadas com sucesso")
}
7. Boas Práticas e Troubleshooting
Versionamento semântico: Nomeie migrations com prefixo numérico sequencial. Migrações irreversíveis (como remoção de colunas) devem ser evitadas ou ter .down.sql bem testado.
Logging: Habilite logs detalhados em desenvolvimento:
import "github.com/golang-migrate/migrate/v4/logger"
m.Log = &logger.Default{}
Problemas comuns:
- Dirty state: Ocorre quando uma migration falha no meio da execução. Solução:
m.Force(version)para marcar como limpa e corrigir manualmente. - Conflitos de versão: Evite editar migrations já aplicadas. Crie uma nova migration corretiva.
- Timeout: Migrações longas (grandes volumes de dados) podem exceder o timeout padrão. Configure
database/sqlcomSetConnMaxLifetime.
Exemplo de tratamento de dirty state:
version, dirty, err := m.Version()
if dirty {
log.Printf("Banco em dirty state na versão %d. Aplicando force...", version)
if err := m.Force(int(version)); err != nil {
log.Fatal("Erro ao forçar versão:", err)
}
// Corrija manualmente o banco antes de continuar
}
Referências
- Documentação oficial do golang-migrate — Repositório oficial com exemplos, drivers suportados e guia de uso completo.
- golang-migrate: Getting Started Guide — Tutorial prático de Tech School Guru com exemplos passo a passo.
- Using pgx with golang-migrate — Guia prático de Alex Edwards sobre migrations com pgx e boas práticas.
- Database Migrations with Go and Docker — Artigo da CloudBees sobre estratégias de migrations em pipelines CI/CD com Docker.
- golang-migrate: Advanced Patterns — Medium post abordando dirty state, locks e patterns avançados de troubleshooting.