Oscar Forner
$(whoami) Projects Resume

Go embed for migrations

Introduction

In this post we will explore an example of a Go binary that combines features such as Go Embed and Go-Migrate to have a self-contained binary that runs the appropriate migrations before starting any actual work.

What is embed?

Go Embed was introduced in Go 1.16 as a core library that allows binaries to contain external files embedded and they are accessible from inside the binary.

There are multiple ways to benefit from this amazing feature, from packing your whole webapp with all its assets, to having binaries containing their DB migrations to always have the correct DB schema to interact with.

Note: I do not recommend to serve all webapp assets from the binary for webapps with high traffic load. Using Nginx, or a CDN would be a better way to do it.

Go-Migrate to run migrations

Go-Migrate is a library to handle all aspects of DB migrations in Go. It is both a CLI tool, and a library.

Go-Migrate CLI tool

Install

$ go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
go: downloading github.com/golang-migrate/migrate v3.5.4+incompatible
go: downloading github.com/lib/pq v1.10.0

Create new migration

$ migrate create -ext sql -dir migrations -seq create_example_table
/home/maitesin/dev/blog/2023_go_embed_for_migrations/migrations/000001_create_example_table.up.sql
/home/maitesin/dev/blog/2023_go_embed_for_migrations/migrations/000001_create_example_table.down.sql

Go-Migrate with Go Embed

The following source code shows how Go-Migrate can use the embedded migrations in the binary.

package main

import (
	"database/sql"
	"embed"
	"fmt"

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

const dbURL = "postgres://postgres:postgres@localhost:54321/examples?sslmode=disable"

//go:embed migrations/*.sql
var migrationsFS embed.FS

func main() {
	dbConn, err := sql.Open("postgres", dbURL)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer dbConn.Close()

	d, err := iofs.New(migrationsFS, "migrations")
	if err != nil {
		fmt.Println(err)
		return
	}

	migrations, err := migrate.NewWithSourceInstance("iofs", d, dbURL)
	if err != nil {
		fmt.Println(err)
		return
	}

	err = migrations.Up()
	if err != nil && err.Error() != "no change" {
		fmt.Println(err)
		return
	}

	// Here goes your awesome code to get rich :D
}

Extra setup

Obviously you will need a DB - PostgreSQL in this example - to connect to in order to run the migrations. And the migration files to be placed in the correct location for the go binary to find them.

For the example used in this post we have the following filesystem hierarchy:

.
├── docker-compose.yml
├── go.mod
├── go.sum
├── main.go
└── migrations
    ├── 000001_create_examples_table.down.sql
    └── 000001_create_examples_table.up.sql

Docker for Postgres

When I am developing, I tend to work with a DB locally running inside Docker. So, for this example I have used the same approach. The following is the dockerfile that sets the PostgreSQL instance up.

Please note that there is no entry point set up for it in order to do a migration. Since the migration will be done completely from the Go binary.

version: "3"

services:
  db:
    image: postgres:${POSTGRES_VERSION:-15}
    environment:
      POSTGRES_USER: ${DB_USERNAME:-postgres}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
      POSTGRES_DB: ${DB_NAME:-examples}
    ports:
      - "${DB_HOST:-127.0.0.1}:${DB_PORT:-54321}:5432"
    command: ["postgres", "-c", "log_statement=all"]

Migration files

The migration file to create the examples table is:

CREATE TABLE IF NOT EXISTS examples(
   id serial PRIMARY KEY,
   name VARCHAR (50) UNIQUE NOT NULL
);

The rollback file to revert the creation of the examples table is:

DROP TABLE IF EXISTS examples;

Execution

If we put all of the above steps together, we can get an execution like the one shown in the following gif:

Conclusion

As shown in this post, we can use the embed library and go-migrate tool to build binaries that can run and verify DB migrations.

This is useful in multiple scenarios, such as building binaries for sidecar pods for Kubernetes deployments, or microservices that are self-contained with the DB migrations they require to be run.