Customer can now set their info (name, address) and print invoices with optional VAT number.

Go

How to handle environment variables in Go (via .env file)

Dominic St-Pierre

Dominic St-Pierre

Software systems builder

I show how I use Makefile without dependency for env vars in Go.

How to handle environment variables in Go (via .env file)

Handling environment variables might sound straightforward, and to be frank, it is, but I'd like to show you how I'm doing this and, most importantly, how I do this without any 3rd party dependencies.

Makefile for the win

Many packages (projects) use Makefile in the Go ecosystem as their build system. I recognize that Makefile might seem old school, if not wholly outdated, for younger programmers.

To me, the fact that the make command is available on all Linux distros, macOS, and Windows via the Linux subsystem for Windows is an excellent reason to give it a shot if you haven't already.

Here's an example of importing a .env file into my Makefile to ensure all environment variables are available to all commands inside my file.

include .env
export $(shell sed 's/=.*//' .env)

start:
  go build -o server && ./server

A .env file example:

SOME_VAR_NAME=value-here
ANOTHER_VAR_NAME=maybe-a-dev-api-key

Config struct

My go-to way of handling configuration is to create a config package. This package has a ConfigData structure that contains all the fields filled with values from the .env file.

package config
type ConfigData struct {
  SomeVarName string
  AnotherVarName string
}

var config ConfigData
func Load() {
  config = ConfigData{
    SomeVarName: os.Getenv("SOME_VAR_NAME"),
    AnotherVarName: os.Getenv("ANOTHER_VAR_NAME"),
        }
}

func Get() ConfigData {
  return config
}

The structure gets its values assigned in the Load function. External packages will be able to get the configuration via the Get function, which is similar to a singleton returning the private package variable config directly.

Here's an example of an external package using the config package:

package other

import "github.com/you/your-root-pkg/config"

func main() {
  config.Load()
  print(config.Get().SomeVarName)
}

Type safety

I've mostly seen string values for environmental variables. However, sometimes, one would need to ensure the validity and use different types for specific values.

In that case, I parse the value(s) before setting them in the struct like the following example:

func Load() ConfigData {
  sendEmail := os.Getenv("SEND_EMAILS") == "true"
  config = ConfigData{
    SomeVarName: os.Getenv("SOME_VAR_NAME"),
    AnotherVarName: os.Getenv("ANOTHER_VAR_NAME"),
    SendEmail: sendEmail,
  }
}

If you find yourself having a lot of values with integers or floats, consider using a third party to help manage that.

But you might also ensure those values are still needed in environment variables. We want to keep process configuration and application configuration distinct. Sometimes, it's OK to have application configuration in a flat file or a database. Not all config are environment variables.

Am I anti-dependencies?

Not at all. I'm cautious about the fact of blindly bringing dependencies to a project. As someone who handles long-lived software system I've been bitten, by dependencies in the past. You develop some protection and ensure that dependencies are added for the right reasons.

And for simpler things like handling environment variables, well, I prefer Makefile.