How to handle environment variables in Go (via .env file)
Software systems builder
I show how I use Makefile without dependency for env vars in Go.
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.