DEV Community

Shrijith Venkatramana
Shrijith Venkatramana

Posted on

4 3 3 3 3

Level Up Your Database Schema with Golang-Migrate

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 schema changes can be a pain. You’re building a Go app, the code’s flowing, but then you need to tweak your database—add a column, drop a table, or rename something. Doing this manually is error-prone and doesn’t scale. That’s where golang-migrate, a Go library for database migrations, saves the day. It lets you version your schema, apply changes systematically, and roll back if things go south.

This post dives into using golang-migrate to manage your database schema. We’ll cover setup, writing migrations, running them, and handling real-world scenarios. Expect practical examples, code you can actually run, and tips to avoid common pitfalls. Let’s get your database evolving smoothly.

Why Database Migrations Matter

If you’ve ever manually altered a database schema in production, you know it’s like defusing a bomb. One wrong move, and your app crashes or data gets corrupted. Migrations solve this by providing a structured, repeatable way to update your schema. With golang-migrate, you define changes as versioned scripts, apply them in order, and track what’s been done.

Key benefits:

  • Version control for your schema, just like your code.
  • Consistency across environments (dev, staging, prod).
  • Rollbacks for when things go wrong.
  • Support for multiple databases (Postgres, MySQL, SQLite, etc.).

This post uses Postgres for examples, but golang-migrate supports many databases. Check the full list here.

Setting Up Golang-Migrate

To start, you need the golang-migrate CLI and library. The CLI helps create and run migrations, while the library integrates migrations into your Go app.

Install the CLI

Run this to install the CLI:

go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
Enter fullscreen mode Exit fullscreen mode

The postgres tag ensures support for Postgres. Swap it for mysql, sqlite, or others based on your database.

Add the Library

In your Go project, add the library:

go get -u github.com/golang-migrate/migrate/v4
Enter fullscreen mode Exit fullscreen mode

Project Structure

Create a migrations folder in your project to store migration files. Each file follows the naming convention VERSION_description.up.sql (for applying changes) and VERSION_description.down.sql (for rollbacks). Example:

project/
├── main.go
├── migrations/
│   ├── 202505310001_create_users_table.up.sql
│   ├── 202505310001_create_users_table.down.sql
Enter fullscreen mode Exit fullscreen mode

Pro tip: Use timestamps for VERSION (e.g., 202505310001) to avoid conflicts and keep migrations chronological.

Writing Your First Migration

Let’s create a migration to set up a users table. Run this CLI command to generate migration files:

migrate create -ext sql -dir migrations -seq create_users_table
Enter fullscreen mode Exit fullscreen mode

This creates two files in migrations/:

  • 202505310001_create_users_table.up.sql
  • 202505310001_create_users_table.down.sql

Up Migration

Edit 202505310001_create_users_table.up.sql:

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Output: Creates a users table with id, username, email, and created_at columns.
Enter fullscreen mode Exit fullscreen mode

Down Migration

Edit 202505310001_create_users_table.down.sql:

DROP TABLE users;

-- Output: Drops the users table if migration is rolled back.
Enter fullscreen mode Exit fullscreen mode

Key point: The down migration should reverse the up migration exactly. Test both to ensure they work as expected.

Running Migrations in Your Go App

Now, let’s integrate migrations into your Go app. Below is a complete example that connects to Postgres and applies migrations.

package main

import (
    "database/sql"
    "log"

    "github.com/golang-migrate/migrate/v4"
    "github.com/golang-migrate/migrate/v4/database/postgres"
    _ "github.com/golang-migrate/migrate/v4/source/file"
    _ "github.com/lib/pq"
)

func main() {
    // Connect to Postgres
    db, err := sql.Open("postgres", "postgres://user:password@localhost:5432/mydb?sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Initialize migration driver
    driver, err := postgres.WithInstance(db, &postgres.Config{})
    if err != nil {
        log.Fatal(err)
    }

    // Set up migrator
    m, err := migrate.NewWithDatabaseInstance(
        "file://migrations",
        "postgres",
        driver,
    )
    if err != nil {
        log.Fatal(err)
    }

    // Apply migrations
    err = m.Up()
    if err != nil && err != migrate.ErrNoChange {
        log.Fatal(err)
    }

    log.Println("Migrations applied successfully")
}

// Output: Applies all pending migrations or logs "Migrations applied successfully" if no changes.
Enter fullscreen mode Exit fullscreen mode

How It Works

  • Connects to your Postgres database using lib/pq.
  • Initializes a migration driver for Postgres.
  • Points to the migrations folder using file://migrations.
  • Runs m.Up() to apply all pending migrations.

Note: migrate.ErrNoChange means no new migrations were applied (e.g., already up-to-date). Always handle this error to avoid false positives.

Handling Schema Changes Like a Pro

Let’s say your app evolves, and you need to add a last_login column to the users table. Create a new migration:

migrate create -ext sql -dir migrations -seq add_last_login_to_users
Enter fullscreen mode Exit fullscreen mode

Up Migration

202505310002_add_last_login_to_users.up.sql:

ALTER TABLE users ADD COLUMN last_login TIMESTAMP;

-- Output: Adds last_login column to users table.
Enter fullscreen mode Exit fullscreen mode

Down Migration

202505310002_add_last_login_to_users.down.sql:

ALTER TABLE users DROP COLUMN last_login;

-- Output: Removes last_login column from users table.
Enter fullscreen mode Exit fullscreen mode

Applying the Change

Run the migrations again using the Go code above or the CLI:

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

Pro tip: Test migrations in a dev environment first. Use a tool like pgAdmin to inspect the schema after applying.

Rolling Back When Things Go Wrong

Mistakes happen. Maybe you added a column with the wrong type. Golang-migrate makes rollbacks easy with the down migrations.

To roll back the last migration:

err = m.Down()
if err != nil && err != migrate.ErrNoChange {
    log.Fatal(err)
}
Enter fullscreen mode Exit fullscreen mode

Or via CLI:

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

Example Rollback

If you applied the last_login migration but realize it should be NOT NULL, roll it back, edit the up migration:

ALTER TABLE users ADD COLUMN last_login TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;

-- Output: Adds last_login column with NOT NULL and default value.
Enter fullscreen mode Exit fullscreen mode

Then reapply the migration.

Key point: Always write down migrations, even if you think you won’t need them. They’re your safety net.

Managing Migrations in a Team

In a team, multiple developers might create migrations simultaneously, leading to conflicts. Here’s how to avoid chaos:

Practice Why It Helps
Use timestamped versions Prevents naming collisions (e.g., 202505310001 vs 202505310002).
Lock the database Golang-migrate locks the database during migrations to prevent concurrent runs.
Test migrations locally Catch errors before they hit production.
Document migration intent Add comments in SQL files to explain complex changes.

Example Comment in an up migration:

-- Adds index to improve query performance on username lookups
CREATE INDEX idx_users_username ON users (username);

-- Output: Creates an index on the username column.
Enter fullscreen mode Exit fullscreen mode

Pro tip: Use a CI/CD pipeline to run migrations automatically on deployment, but ensure only one process runs migrations at a time.

Handling Complex Migrations

Sometimes, you need more than simple CREATE or ALTER statements. For example, you might need to migrate data when adding a new column.

Scenario: Splitting Full Name into First/Last

Suppose your users table has a full_name column, and you want to split it into first_name and last_name. Create a migration:

migrate create -ext sql -dir migrations -seq split_user_names
Enter fullscreen mode Exit fullscreen mode

Up Migration

202505310003_split_user_names.up.sql:

ALTER TABLE users
    ADD COLUMN first_name VARCHAR(50),
    ADD COLUMN last_name VARCHAR(50);

UPDATE users
SET
    first_name = SPLIT_PART(full_name, ' ', 1),
    last_name = SPLIT_PART(full_name, ' ', 2)
WHERE full_name IS NOT NULL;

ALTER TABLE users
    DROP COLUMN full_name;

-- Output: Adds first_name and last_name, populates them from full_name, then drops full_name.
Enter fullscreen mode Exit fullscreen mode

Down Migration

202505310003_split_user_names.down.sql:

ALTER TABLE users
    ADD COLUMN full_name VARCHAR(100);

UPDATE users
SET full_name = CONCAT(first_name, ' ', last_name)
WHERE first_name IS NOT NULL OR last_name IS NOT NULL;

ALTER TABLE users
    DROP COLUMN first_name,
    DROP COLUMN last_name;

-- Output: Restores full_name, populates it from first_name and last_name, then drops them.
Enter fullscreen mode Exit fullscreen mode

Note: This assumes names are space-separated. Real-world data might need more complex logic (e.g., handling missing spaces).

Best Practices for Smoother Migrations

Here’s a quick checklist to keep your migrations headache-free:

Best Practice Details
Keep migrations small One change per migration (e.g., add column, then index in separate files).
Test both up and down Run up and down locally to verify reversibility.
Backup before production Always back up your database before applying migrations in production.
Use transactions when possible Wrap complex migrations in BEGIN/COMMIT to ensure atomicity.
Monitor migration performance Large data migrations can be slow; test and optimize (e.g., batch updates).

For advanced tips, check the golang-migrate docs.

What’s Next for Your Database

Using golang-migrate transforms schema management from a risky chore to a controlled, repeatable process. Start by setting up the CLI and library, writing small, focused migrations, and integrating them into your Go app. Test thoroughly in development, handle rollbacks gracefully, and follow best practices to keep your team in sync.

As your app grows, you’ll face more complex migrations—data transformations, index optimizations, or even database refactoring. Golang-migrate handles these with ease, letting you focus on building features. Experiment with it in a side project, and you’ll see how it simplifies schema evolution.

Got a tricky migration scenario? Drop a comment on Dev.to, and let’s figure it out together.

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

Top comments (0)

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

Explore this insightful write-up, celebrated by our thriving DEV Community. Developers everywhere are invited to contribute and elevate our shared expertise.

A simple "thank you" can brighten someone’s day—leave your appreciation in the comments!

On DEV, knowledge-sharing fuels our progress and strengthens our community ties. Found this useful? A quick thank you to the author makes all the difference.

Okay