Go

Migrations em Go com golang-migrate: Versionando o Banco de Dados: Do Básico ao Avançado

14 min de leitura

Migrations em Go com golang-migrate: Versionando o Banco de Dados: Do Básico ao Avançado

O que são Migrations e Por que Você Precisa Delas Migrations são scripts versionados que rastreiam e aplicam mudanças no esquema do banco de dados de forma controlada e reproduzível. Assim como você controla versões de código com Git, as migrations permitem que você controle versões de estrutura de dados. Sem elas, diferentes ambientes (desenvolvimento, staging, produção) podem ficar dessincronizados, ou pior, você perde o histórico de mudanças que ocasionaram determinado estado do banco. A grande vantagem é a rastreabilidade. Você sabe exatamente quando uma coluna foi adicionada, quando um índice foi criado, e pode reverter mudanças se necessário. Em um time de desenvolvimento, isso evita aquele caos onde alguém muda o banco localmente e esquece de avisar os colegas. O golang-migrate é uma ferramenta robusta, escrita em Go, que implementa esse conceito de forma elegante e performática, funcionando com diversos bancos de dados (PostgreSQL, MySQL, SQLite, etc.). Instalando e Configurando o golang-migrate Instalação A forma mais direta é usar

<h2>O que são Migrations e Por que Você Precisa Delas</h2>

<p>Migrations são scripts versionados que rastreiam e aplicam mudanças no esquema do banco de dados de forma controlada e reproduzível. Assim como você controla versões de código com Git, as migrations permitem que você controle versões de estrutura de dados. Sem elas, diferentes ambientes (desenvolvimento, staging, produção) podem ficar dessincronizados, ou pior, você perde o histórico de mudanças que ocasionaram determinado estado do banco.</p>

<p>A grande vantagem é a <strong>rastreabilidade</strong>. Você sabe exatamente quando uma coluna foi adicionada, quando um índice foi criado, e pode reverter mudanças se necessário. Em um time de desenvolvimento, isso evita aquele caos onde alguém muda o banco localmente e esquece de avisar os colegas. O golang-migrate é uma ferramenta robusta, escrita em Go, que implementa esse conceito de forma elegante e performática, funcionando com diversos bancos de dados (PostgreSQL, MySQL, SQLite, etc.).</p>

<h2>Instalando e Configurando o golang-migrate</h2>

<h3>Instalação</h3>

<p>A forma mais direta é usar o <code>go install</code> para instalar a ferramenta CLI na sua máquina:</p>

<pre><code class="language-bash">go install -tags &#039;postgres,mysql,sqlite3&#039; github.com/golang-migrate/migrate/v4/cmd/migrate@latest</code></pre>

<p>O flag <code>-tags</code> especifica quais drivers você deseja suportar. Se você trabalha apenas com PostgreSQL, pode simplificar para:</p>

<pre><code class="language-bash">go install -tags &#039;postgres&#039; github.com/golang-migrate/migrate/v4/cmd/migrate@latest</code></pre>

<p>Verifique se a instalação funcionou:</p>

<pre><code class="language-bash">migrate -version</code></pre>

<p>Você também precisa adicionar o pacote ao seu projeto Go:</p>

<pre><code class="language-bash">go get -u github.com/golang-migrate/migrate/v4</code></pre>

<h3>Estrutura de Diretórios</h3>

<p>Crie um diretório para armazenar suas migrations. A convenção é usar <code>migrations/</code> na raiz do projeto:</p>

<pre><code>seu-projeto/

├── migrations/

│ ├── 000001_criar_usuarios.up.sql

│ ├── 000001_criar_usuarios.down.sql

│ ├── 000002_adicionar_email.up.sql

│ └── 000002_adicionar_email.down.sql

├── main.go

└── go.mod</code></pre>

<p>Cada migration possui dois arquivos: <code>.up.sql</code> (aplica a mudança) e <code>.down.sql</code> (reverte a mudança). O número no início garante a ordem de execução. Essa estrutura simples, mas poderosa, evita conflitos e deixa o histórico cristalino.</p>

<h2>Criando e Gerenciando Migrations</h2>

<h3>Criando Sua Primeira Migration</h3>

<p>Use o comando <code>migrate</code> para gerar os arquivos automaticamente:</p>

<pre><code class="language-bash">migrate create -ext sql -dir migrations -seq criar_tabela_usuarios</code></pre>

<p>Isso cria os arquivos <code>000001_criar_tabela_usuarios.up.sql</code> e <code>000001_criar_tabela_usuarios.down.sql</code>. O argumento <code>-seq</code> usa numeração sequencial automática.</p>

<p>Agora, edite o arquivo <code>.up.sql</code>:</p>

<pre><code class="language-sql">-- migrations/000001_criar_tabela_usuarios.up.sql

CREATE TABLE usuarios (

id SERIAL PRIMARY KEY,

nome VARCHAR(255) NOT NULL,

email VARCHAR(255) UNIQUE NOT NULL,

criado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP

);</code></pre>

<p>E o <code>.down.sql</code> para reverter:</p>

<pre><code class="language-sql">-- migrations/000001_criar_tabela_usuarios.down.sql

DROP TABLE usuarios;</code></pre>

<h3>Adicionando Mais Mudanças</h3>

<p>Conforme seu projeto evolui, você cria novas migrations. Por exemplo, adicionar um campo de senha:</p>

<pre><code class="language-bash">migrate create -ext sql -dir migrations -seq adicionar_senha_usuarios</code></pre>

<p>Editando o <code>.up.sql</code>:</p>

<pre><code class="language-sql">-- migrations/000002_adicionar_senha_usuarios.up.sql

ALTER TABLE usuarios ADD COLUMN senha_hash VARCHAR(255) NOT NULL DEFAULT &#039;&#039;;

ALTER TABLE usuarios ADD COLUMN atualizado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP;</code></pre>

<p>E o <code>.down.sql</code>:</p>

<pre><code class="language-sql">-- migrations/000002_adicionar_senha_usuarios.down.sql

ALTER TABLE usuarios DROP COLUMN senha_hash;

ALTER TABLE usuarios DROP COLUMN atualizado_em;</code></pre>

<p>Essa abordagem granular permite que você entenda exatamente o que mudou em cada etapa. Se um colega pede para você reverter apenas a adição de uma coluna, você sabe qual migration foi responsável.</p>

<h2>Integrando Migrations no Código Go</h2>

<h3>Executando Migrations Programaticamente</h3>

<p>A verdadeira mágica acontece quando você integra as migrations diretamente no seu aplicativo. Assim, ao iniciar a aplicação, as migrations pendentes são executadas automaticamente. Aqui está um padrão robusto:</p>

<pre><code class="language-go">package main

import (

&quot;fmt&quot;

&quot;log&quot;

&quot;github.com/golang-migrate/migrate/v4&quot;

_ &quot;github.com/golang-migrate/migrate/v4/database/postgres&quot;

_ &quot;github.com/golang-migrate/migrate/v4/source/file&quot;

)

func RunMigrations(databaseURL string) error {

m, err := migrate.New(

&quot;file://migrations&quot;,

databaseURL,

)

if err != nil {

return fmt.Errorf(&quot;erro ao criar migrate: %w&quot;, err)

}

defer m.Close()

// Aplica todas as migrations pendentes

if err := m.Up(); err != nil &amp;&amp; err != migrate.ErrNoChange {

return fmt.Errorf(&quot;erro ao executar migrations: %w&quot;, err)

}

log.Println(&quot;Migrations executadas com sucesso&quot;)

return nil

}

func main() {

databaseURL := &quot;postgres://user:password@localhost:5432/meu_banco?sslmode=disable&quot;

if err := RunMigrations(databaseURL); err != nil {

log.Fatal(err)

}

// Seu código da aplicação continua aqui

log.Println(&quot;Aplicação iniciada&quot;)

}</code></pre>

<p>Note que os imports com underscore (<code>_</code>) são necessários — eles registram os drivers sem ocupar espaço no namespace. O método <code>Up()</code> aplica todas as migrations não executadas ainda. O erro <code>migrate.ErrNoChange</code> é esperado quando o banco já está atualizado, então ignoramos ele.</p>

<h3>Integrando com um Banco de Dados Real</h3>

<p>Na prática, você provavelmente já tem uma conexão com o banco. Aqui está um exemplo mais realista usando <code>database/sql</code>:</p>

<pre><code class="language-go">package main

import (

&quot;database/sql&quot;

&quot;fmt&quot;

&quot;log&quot;

&quot;github.com/golang-migrate/migrate/v4&quot;

_ &quot;github.com/golang-migrate/migrate/v4/database/postgres&quot;

_ &quot;github.com/golang-migrate/migrate/v4/source/file&quot;

_ &quot;github.com/lib/pq&quot;

)

func main() {

// Conexão com o banco

dsn := &quot;postgres://user:password@localhost:5432/meu_banco?sslmode=disable&quot;

db, err := sql.Open(&quot;postgres&quot;, dsn)

if err != nil {

log.Fatalf(&quot;Erro ao conectar ao banco: %v&quot;, err)

}

defer db.Close()

// Verificar conexão

if err := db.Ping(); err != nil {

log.Fatalf(&quot;Erro ao pingar banco: %v&quot;, err)

}

// Executar migrations

m, err := migrate.New(

&quot;file://migrations&quot;,

dsn,

)

if err != nil {

log.Fatalf(&quot;Erro ao inicializar migrate: %v&quot;, err)

}

defer m.Close()

if err := m.Up(); err != nil &amp;&amp; err != migrate.ErrNoChange {

log.Fatalf(&quot;Erro ao aplicar migrations: %v&quot;, err)

}

log.Println(&quot;Banco de dados atualizado com sucesso&quot;)

// Sua lógica de aplicação aqui

// Por exemplo, inserir um usuário:

var id int

err = db.QueryRow(

&quot;INSERT INTO usuarios (nome, email, senha_hash) VALUES ($1, $2, $3) RETURNING id&quot;,

&quot;João Silva&quot;,

&quot;joao@example.com&quot;,

&quot;hash_seguro_aqui&quot;,

).Scan(&amp;id)

if err != nil {

log.Fatalf(&quot;Erro ao inserir usuário: %v&quot;, err)

}

fmt.Printf(&quot;Usuário criado com ID: %d\n&quot;, id)

}</code></pre>

<h3>Revertendo Migrations</h3>

<p>Às vezes você precisa reverter mudanças. Use o método <code>Down()</code>:</p>

<pre><code class="language-go">func RevertLastMigration(databaseURL string) error {

m, err := migrate.New(

&quot;file://migrations&quot;,

databaseURL,

)

if err != nil {

return fmt.Errorf(&quot;erro ao criar migrate: %w&quot;, err)

}

defer m.Close()

// Reverte a última migration aplicada

if err := m.Steps(-1); err != nil {

return fmt.Errorf(&quot;erro ao reverter migration: %w&quot;, err)

}

log.Println(&quot;Migration revertida com sucesso&quot;)

return nil

}</code></pre>

<p>O método <code>Steps(-1)</code> reverte uma migration. Se você quiser reverter 3 migrations, use <code>Steps(-3)</code>. Isso é particularmente útil em desenvolvimento quando você comete um erro e precisa corrigir rapidamente.</p>

<h2>Boas Práticas e Padrões Avançados</h2>

<h3>Versionamento Semântico em Migrations</h3>

<p>Embora o golang-migrate use números sequenciais, você pode adotar uma convenção de nomenclatura mais descritiva. Alguns projetos usam timestamps ou datas:</p>

<pre><code class="language-bash">migrate create -ext sql -dir migrations -seq 20240115_criar_tabela_produtos</code></pre>

<p>Isso resulta em nomes como <code>000001_20240115_criar_tabela_produtos.up.sql</code>. Combine com mensagens claras: o nome deve descrever a mudança, não ser genérico como &quot;adicionar_coluna&quot;.</p>

<h3>Idempotência e Segurança</h3>

<p>Escritas migrations de forma que sejam seguras de executar múltiplas vezes. Use <code>IF NOT EXISTS</code> e <code>IF EXISTS</code>:</p>

<pre><code class="language-sql">-- Segura: não falha se a tabela já existe

CREATE TABLE IF NOT EXISTS usuarios (

id SERIAL PRIMARY KEY,

email VARCHAR(255) UNIQUE NOT NULL

);

-- Segura: não falha se a coluna não existe

ALTER TABLE usuarios ADD COLUMN IF NOT EXISTS telefone VARCHAR(20);

-- No down.sql, também seja explícito

DROP TABLE IF EXISTS usuarios;</code></pre>

<h3>Separando Concerns: Criação vs. Modificação</h3>

<p>Para grandes projetos, separe migrações que criam estruturas daquelas que modificam dados. Por exemplo:</p>

<pre><code class="language-sql">-- 000003_criar_tabela_pedidos.up.sql

CREATE TABLE pedidos (

id SERIAL PRIMARY KEY,

usuario_id INTEGER NOT NULL REFERENCES usuarios(id),

total DECIMAL(10, 2),

criado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP

);

-- 000004_popular_pedidos_historicos.up.sql

INSERT INTO pedidos (usuario_id, total)

SELECT id, 0 FROM usuarios WHERE id NOT IN (SELECT DISTINCT usuario_id FROM pedidos);</code></pre>

<p>Essa separação torna mais fácil entender o que cada migration faz e reverter mudanças de dados sem afetar a estrutura.</p>

<h3>Tratando Erros em Migrations Críticas</h3>

<p>Para migrations que podem falhar, adicione tratamento robusto no seu código Go:</p>

<pre><code class="language-go">func SafeRunMigrations(databaseURL string) error {

m, err := migrate.New(

&quot;file://migrations&quot;,

databaseURL,

)

if err != nil {

return fmt.Errorf(&quot;erro ao criar migrate: %w&quot;, err)

}

defer m.Close()

// Obter versão atual

version, dirty, err := m.Version()

if err != nil &amp;&amp; err != migrate.ErrNilVersion {

return fmt.Errorf(&quot;erro ao obter versão: %w&quot;, err)

}

log.Printf(&quot;Versão atual do banco: %d (dirty: %v)&quot;, version, dirty)

// Se o banco está marcado como &quot;sujo&quot; (migration falhou no meio), não continua

if dirty {

return fmt.Errorf(&quot;banco marcado como sujo; verifique a última migration antes de continuar&quot;)

}

if err := m.Up(); err != nil &amp;&amp; err != migrate.ErrNoChange {

return fmt.Errorf(&quot;erro ao aplicar migrations: %w&quot;, err)

}

return nil

}</code></pre>

<p>O atributo &quot;dirty&quot; é acionado quando uma migration falha no meio. Você precisa resolver manualmente ou forçar uma versão antes de continuar.</p>

<h2>Conclusão</h2>

<p>Aprendemos que <strong>migrations não são apenas scripts SQL</strong>, mas sim uma estratégia de versionamento que mantém seu banco de dados sincronizado e rastreável. O golang-migrate oferece uma solução elegante que integra-se perfeitamente em aplicações Go, permitindo tanto execução automática ao iniciar a aplicação quanto reversão controlada quando necessário. Por fim, <strong>boas práticas como idempotência, nomenclatura clara e separação de concerns</strong> transformam migrations de um incômodo administrativo em um ativo valioso para manutenção e escalabilidade do seu projeto.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://github.com/golang-migrate/migrate" target="_blank" rel="noopener noreferrer">golang-migrate - Documentação Oficial</a></li>

<li><a href="https://www.postgresql.org/docs/current/ddl.html" target="_blank" rel="noopener noreferrer">PostgreSQL - Documentação de DDL (Data Definition Language)</a></li>

<li><a href="https://docs.liquibase.com/concepts/changelogs/working-with-changelogs.html" target="_blank" rel="noopener noreferrer">Database Migration Best Practices - Liquibase</a></li>

<li><a href="https://golang.org/doc/database/sql-tutorial" target="_blank" rel="noopener noreferrer">Go Database/SQL Tutorial</a></li>

<li><a href="https://martinfowler.com/articles/evodb.html" target="_blank" rel="noopener noreferrer">Martin Fowler - Evolutionary Database Design</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Erros em Go: error Interface, Sentinel Errors e Erros Customizados na Prática
Erros em Go: error Interface, Sentinel Errors e Erros Customizados na Prática

O Mecanismo de Erros em Go Go adota uma abordagem pragmática para tratamento...

Guia Completo de gRPC em Go: Protocol Buffers, Streaming e Interceptors
Guia Completo de gRPC em Go: Protocol Buffers, Streaming e Interceptors

O que é gRPC e Por Que Importa gRPC é um framework de chamada de procedimento...

Pacote io em Go: Readers, Writers e a Filosofia de Streams na Prática
Pacote io em Go: Readers, Writers e a Filosofia de Streams na Prática

A Filosofia de Streams em Go A programação tradicional frequentemente trabalh...