Build a Rails-Like Migration Runner for Your Go Projects
By: Angus Morrison
Tags: Ruby, Golang, Ruby on Rails, Programming, Go

2. ‘migrate.go’

I’ve named the runner migrate.go , and it lives at cmd/migrate/migrate.go under the project root. Eventually, we’ll use make commands similar to Rail’s rake db:migrate to run this script with the appropriate arguments.

Let’s start with the main function:

On lines 3-13, we specify and parse flags that control where our program should look for the environment config file. In this case, I’ve set the defaults to point to environment.yaml in the root of our project.

Next, we check to make sure the script has been called with at least one argument: the command getting the Migrate package to run. Some of these commands (like (m* Migrate) Steps(n int) , for example), take arguments of their own, but we don’t check for that yet.

On lines 24-29, we load the environment file described by our flags. Behind the scenes, this is done by a package called Viper. More on that shortly.

With the environment successfully loaded, we’re in a position to run the migration with the specified command and any arguments that have been passed.

Let’s take a closer look at loadEnvVars and runMigration .

'loadEnvVars'

loadEnvVars takes the configName and configPath flags we parsed in main and returns a pointer to a viper.Viper .

Viper is a high-powered configuration package for Go applications and, as such, it’s overkill for a script like this. However, I’m planning to use Viper elsewhere in my project, so it made sense to pull it in here too. If you’d prefer something lightweight, consider envconfig, GoDotEnv, or simply parsing the environment file yourself.

On lines 12-14, we configure Viper to read from a file called environment.yaml located in the project root. Notice that I didn’t create a flag called configType — I’ve hardcoded yaml because I know my config files will always be YAML. You may decide differently.

viper.ReadInConfig causes Viper to read the env vars contained in the source file and create a *viper.Viper . When we want to access an environment variable, we query this struct with methods like (v *Viper) Get(key string) instead of running os.Getenv .

But we’re not done! Viper has loaded the whole environment.yaml file, including configuration for test , production , and any other group you might have stored in there. That’s why the first thing loadEnvVars does is to look for the target environment with os.Getenv("FB05_ENV") . We pass this value to viper.Sub to return a *viper.Viper that contains only the subset of environment variables that match our target environment.

What happens to this *viper.Viper when loadEnvVars returns? You’ll notice that I don’t declare any new variables in when I call env, err = loadEnvVars(*configName, *configPath) on main:25 . env is actually a global variable declared outside of main : var env *viper.Viper . That way, it’s accessible anywhere in the script, just like os.Getenv .

‘runMigration’

This is a chunky one. Hold tight:

First up, we use the Migration package to create a new *migrate.Migration . There’s some complexity in the setup here because you have to register different subpackages of Migrate by blank importing them depending on where your migrations are stored and which DBMS you’re using.

I’m storing my migrations locally, and my drug of choice is PostgreSQL. So my imports look like this:

The first argument to migrate.New is the source : the location your migration files are stored in. Mine are at db/migrations under my project root.

The second is the database URL, and this is the reason why loading environment variables before running your migrations is so valuable:

Imagine having to type this out every time you wanted to run a migration. Imagine having to run your migrations by hand in production. These are the problems this miniproject sets out to solve.

After creating the *migrate.Migration and validating that any numeric arguments provided are indeed numbers, we switch using the specified command and trigger the corresponding Migrate method.

I won’t repeat the documentation here, but as an example, (m *Migration) Up() migrates the database forward through every up migration in your migrations folder, while (m *Migration) Down() does the reverse. That’s something to bear in mind if you’re coming from Rails — there’s no single change method here. Each migration must have separate up and down SQL files to apply and reverse the change, respectively.

A simple up migration might look like:

While the corresponding down migration would look like:

(If you hadn’t already guessed, I’m building an old-school Facebook clone).