DEV Community

Shrijith Venkatramana
Shrijith Venkatramana

Posted on

14 2 2 3 2

Best Database Migration Tools for Golang

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 or goose 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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Run the migration:

goose -dir migrations postgres "user=postgres password=secret dbname=mydb sslmode=disable" up
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Run the migration:

migrate -path migrations -database "postgres://postgres:secret@localhost:5432/mydb?sslmode=disable" up
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Tiger Data image

🐯 🚀 Timescale is now TigerData

Building the modern PostgreSQL for the analytical and agentic era.

Read more

Top comments (2)

Collapse
 
abdul_basith_db7ce5363bd5 profile image
Abdul Basith

Is any rollback mechanism?

Collapse
 
shrsv profile image
Shrijith Venkatramana

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.

Tiger Data image

🐯 🚀 Timescale is now TigerData: Building the Modern PostgreSQL for the Analytical and Agentic Era

We’ve quietly evolved from a time-series database into the modern PostgreSQL for today’s and tomorrow’s computing, built for performance, scale, and the agentic future.

So we’re changing our name: from Timescale to TigerData. Not to change who we are, but to reflect who we’ve become. TigerData is bold, fast, and built to power the next era of software.

Read more

👋 Kindness is contagious

Dive into this thoughtful piece, beloved in the supportive DEV Community. Coders of every background are invited to share and elevate our collective know-how.

A sincere "thank you" can brighten someone's day—leave your appreciation below!

On DEV, sharing knowledge smooths our journey and tightens our community bonds. Enjoyed this? A quick thank you to the author is hugely appreciated.

Okay