Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a first of its kind tool for helping you automatically index API endpoints across all your repositories. LiveAPI helps you discover, understand and use APIs in large tech infrastructures with ease.
Database migrations are a critical part of building and maintaining Go applications. They keep your database schema in sync with your codebase, handle updates, and ensure your app stays reliable as it evolves. Choosing the right migration tool can save you time, reduce errors, and make deployments smoother. In this post, we’ll dive into the best database migration tools for Go, with examples, comparisons, and practical insights to help you pick the right one for your project.
I’ve been through the grind of manual migrations and the chaos of mismatched schemas, so I’ll break down each tool’s strengths, quirks, and use cases in a way that’s easy to follow. Let’s explore the top options, complete with code examples you can actually run.
Why Database Migrations Matter in Go
Before we jump into the tools, let’s talk about why migrations are a big deal. In Go projects, your database schema often evolves—new tables, updated columns, or index changes. Without a migration tool, you’re stuck writing raw SQL, manually tracking versions, or praying your team doesn’t mess up the production database. A good migration tool automates schema changes, tracks history, and ensures consistency across environments.
The tools we’ll cover work well with Go’s ecosystem, integrate with popular databases like PostgreSQL and MySQL, and focus on simplicity or flexibility depending on your needs. Let’s dive into the first tool.
1. Goose: Simple and Lightweight Migrations
Goose is a no-fuss, lightweight migration tool for Go. It’s perfect for developers who want minimal setup and SQL-based migrations without heavy dependencies. Goose supports PostgreSQL, MySQL, SQLite, and more, and it’s easy to integrate into a Go project.
Key Features
- SQL or Go-based migrations: Write migrations in raw SQL or Go code.
-
CLI-driven: Run migrations with simple commands like
goose up
orgoose down
. - No external dependencies: Just a Go binary and your database driver.
Example: Creating a User Table with Goose
Here’s how you can set up a migration to create a users
table in PostgreSQL.
First, install Goose:
go get -u github.com/pressly/goose/v3
Create a migration file (e.g., 20250607101700_create_users_table.sql
):
-- +goose Up
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- +goose Down
DROP TABLE users;
Run the migration:
goose -dir migrations postgres "user=postgres password=secret dbname=mydb sslmode=disable" up
Output: The users
table is created in your PostgreSQL database. Running goose down
will drop it.
When to Use Goose
Goose shines for small to medium projects where you want full control over SQL and a lightweight tool. It’s not ideal for complex migrations requiring programmatic logic, as its Go-based migrations can feel clunky compared to other tools.
2. Migrate: The CLI Powerhouse
Migrate is another popular choice for Go developers. It’s a CLI-first tool that supports a wide range of databases (PostgreSQL, MySQL, SQLite, etc.) and focuses on simplicity and portability. Unlike Goose, Migrate is language-agnostic, so it’s great for teams using multiple languages alongside Go.
Key Features
- Broad database support: Works with almost any database, including cloud-native ones like CockroachDB.
- File-based migrations: Uses plain SQL files with up/down scripts.
- CLI focus: No Go code required, making it easy to use in CI/CD pipelines.
Example: Adding a Posts Table with Migrate
Install Migrate:
go get -u github.com/golang-migrate/migrate/v4
Create a migration file (e.g., 20250607101800_create_posts_table.sql
):
-- +up
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
title VARCHAR(255) NOT NULL,
content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- +down
DROP TABLE posts;
Run the migration:
migrate -path migrations -database "postgres://postgres:secret@localhost:5432/mydb?sslmode=disable" up
Output: The posts
table is created, linked to the users
table via user_id
. Running migrate down
reverses it.
When to Use Migrate
Migrate is ideal for teams needing a language-agnostic tool or working with diverse databases. It’s slightly more complex to set up than Goose but excels in CI/CD integration and cross-database compatibility.
3. Gormigrate: GORM-Friendly Migrations
Gormigrate is a migration library built specifically for GORM, a popular ORM for Go. If your project already uses GORM for database operations, Gormigrate is a natural fit, letting you define migrations in Go code alongside your models.
Key Features
- GORM integration: Leverages GORM’s ORM capabilities for migrations.
- Programmatic migrations: Write migrations in Go, not SQL.
- Rollback support: Easily undo migrations with built-in rollback functions.
Example: Migrating a Products Table with Gormigrate
Here’s a complete example of creating a products
table using Gormigrate.
package main
import (
"log"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"github.com/go-gormigrate/gormigrate/v2"
)
type Product struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(100);not null"`
Price float64
CreatedAt time.Time
}
func main() {
dsn := "host=localhost user=postgres password=secret dbname=mydb port=5432 sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
{
ID: "20250607101900",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&Product{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("products")
},
},
})
if err := m.Migrate(); err != nil {
log.Fatalf("Could not migrate: %v", err)
}
log.Println("Migration completed")
}
// Output: Migration completed
Output: The products
table is created with columns for id
, name
, price
, and created_at
. Running m.Rollback()
drops the table.
When to Use Gormigrate
Use Gormigrate if your project is heavily invested in GORM and you prefer defining migrations in Go. It’s less flexible for raw SQL lovers and not ideal for non-GORM projects.
4. SQLx with Custom Migrations: Roll Your Own
SQLx isn’t a migration tool per se, but it’s a powerful library for working with SQL in Go. You can build a custom migration system using SQLx to execute migration scripts, giving you ultimate flexibility. This approach is best for teams who want full control over their migration logic.
Key Features
- SQLx flexibility: Combine SQLx’s query execution with your own migration tracking.
- Customizable: Build exactly the migration workflow you need.
- No external CLI: Everything runs in your Go code.
Example: Custom Migration with SQLx
Here’s a simple migration system using SQLx to create an orders
table.
package main
import (
"log"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
type Migration struct {
ID string
UpQuery string
}
func main() {
db, err := sqlx.Connect("postgres", "user=postgres password=secret dbname=mydb sslmode=disable")
if err != nil {
log.Fatal(err)
}
// Create migrations table if it doesn't exist
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS migrations (id VARCHAR(50) PRIMARY KEY)`)
if err != nil {
log.Fatal(err)
}
migrations := []Migration{
{
ID: "20250607102000_create_orders",
UpQuery: `
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
total DECIMAL(10,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`,
},
}
for _, m := range migrations {
var exists bool
err := db.Get(&exists, "SELECT EXISTS (SELECT 1 FROM migrations WHERE id = $1)", m.ID)
if err != nil {
log.Fatal(err)
}
if !exists {
_, err := db.Exec(m.UpQuery)
if err != nil {
log.Fatal(err)
}
_, err = db.Exec("INSERT INTO migrations (id) VALUES ($1)", m.ID)
if err != nil {
log.Fatal(err)
}
log.Printf("Applied migration: %s", m.ID)
}
}
}
// Output: Applied migration: 20250607102000_create_orders
Output: The orders
table is created, and the migration is tracked in a migrations
table.
When to Use SQLx
Choose SQLx for custom migration workflows when existing tools don’t fit your needs. It requires more setup but offers unmatched flexibility.
5. Flyway (via Go Integration): Enterprise-Grade Migrations
Flyway is a Java-based migration tool that’s widely used in enterprise settings. While not Go-native, you can integrate it into Go projects using its CLI or by calling its Java library. Flyway is great for teams needing robust versioning and audit-ready migration history.
Key Features
- Versioned migrations: Strict versioning ensures predictable schema changes.
- Enterprise-friendly: Supports complex workflows and multiple environments.
- SQL-based: Write migrations in plain SQL.
Example: Running Flyway with Go
Here’s how you can use Flyway’s CLI in a Go project to create a categories
table.
First, download Flyway and set up a migrations
folder with a file named V1__create_categories_table.sql
:
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Run Flyway via a Go program:
package main
import (
"log"
"os/exec"
)
func main() {
cmd := exec.Command("flyway", "-url=jdbc:postgresql://localhost:5432/mydb", "-user=postgres", "-password=secret", "migrate")
output, err := cmd.CombinedOutput()
if err != nil {
log.Fatalf("Flyway failed: %v\n%s", err, output)
}
log.Println("Flyway migration completed")
log.Println(string(output))
}
// Output: Flyway migration completed
// (Flyway CLI output follows)
Output: The categories
table is created, and Flyway tracks the migration in its flyway_schema_history
table.
When to Use Flyway
Flyway is ideal for enterprise projects or teams already using it in polyglot environments. It’s overkill for small projects due to its Java dependency and setup complexity.
6. Comparing the Tools: Which One Fits Your Project?
To help you choose, here’s a comparison of the tools based on key criteria.
Tool | Database Support | Migration Type | Ease of Use | Best For |
---|---|---|---|---|
Goose | PostgreSQL, MySQL, SQLite, etc. | SQL, Go | High | Small to medium projects |
Migrate | Almost all databases | SQL | Medium | CI/CD pipelines, diverse DBs |
Gormigrate | GORM-supported DBs | Go | High | GORM-based projects |
SQLx (Custom) | Any SQLx-supported DB | SQL, Go | Low | Custom workflows |
Flyway | Many (via JDBC) | SQL | Medium | Enterprise, multi-language teams |
Key takeaway: If you’re unsure, start with Goose for simplicity or Migrate for flexibility. Use Gormigrate for GORM projects, SQLx for custom needs, or Flyway for enterprise-grade requirements.
7. Tips for Smooth Migrations in Go
To wrap up, here are practical tips to make your migrations successful:
-
Version your migrations: Use timestamps or sequential IDs to avoid conflicts (e.g.,
20250607102100
). - Test migrations locally: Always run migrations in a local or staging environment before production.
- Backup your database: Before applying migrations, ensure you have a backup to avoid data loss.
- Use transactions: For complex migrations, wrap changes in transactions to ensure atomicity.
- Document changes: Add comments in migration files to explain the purpose of each change.
Example: Transactional Migration with Goose
Here’s a Goose migration using a transaction for safety:
-- +goose Up
BEGIN;
CREATE TABLE payments (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
amount DECIMAL(10,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO payments (user_id, amount) VALUES (1, 99.99);
COMMIT;
-- +goose Down
DROP TABLE payments;
Output: The payments
table is created, and a sample row is inserted atomically. If anything fails, the transaction rolls back.
What’s Next for Your Go Migrations?
Choosing the right migration tool depends on your project’s size, team, and database needs. Goose and Migrate are great for most Go developers due to their simplicity and SQL focus. Gormigrate is a no-brainer for GORM users, while SQLx offers flexibility for custom setups. Flyway suits enterprise teams needing robust versioning.
Start by experimenting with one tool in a small project. Run the examples above, tweak them for your database, and see what fits your workflow. Whichever tool you pick, prioritize automation, testing, and backup strategies to keep your migrations smooth and your app reliable.
Top comments (2)
Is any rollback mechanism?
Yes. Gomigrate for example asks for both up/down SQL queries to create a migration.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.