Skip to main content
  1. Posts/
  2. Go/

Environment Settings in Go with Viper

··639 words·3 mins·

For many projects, additional configuration is required in order to ensure correct behaviour. Whether its JSON Web Token secrets, database/personal credentials or other customisable settings, reliable and flexible configuration schemes are dependent upon during all stages of software development. In this document, I’ll demonstrate how I use Steve Francia’s viper library, which enables the interchangeable usage of environment variable and a configuration file for the same Go program.

For the purposes of simplicity, I assume that all of the code snippets shown here are part of the main package. However, I recommend authors of large codebases to employ a multi-package taxonomy, such that their code is more organised and maintainable.

Data Structure #

In order to parse and validate the current environment settings, Viper requires a data structure that describes all the properties you require. I usually parse/serialise and validate the current state (as set at during start-up) into a data structure in the main function.

type Configuration struct {
	Secret      string
	URI         string
	Database    string
	Storage     string
	Directories bool
	Port        uint
}

By default, Viper supports parsing/serialising a YAML file (or other common formats of your choice) that matches the data structure semantics:

secret: your_secret
uri: "mongodb://localhost:27017"
database: mongo
storage: .
directories: false
port: 4100

Default Values #

The variable that stores the parsed/serialised state of the settings can be initialised with default values that correspond to unset fields. These default values should be clearly documented to users in order to reduce potential friction that might arise with configuring an unfamiliar program.

var configuration = Configuration{
	URI:         "mongodb://localhost:27017",
	Database:    "mongo",
	Storage:     ".",
	Directories: false,
	Port:        4100,
}

Initialising Viper #

I like to include the Viper initialisation code in an init function in the main package or in a package that is loaded by it, such that non-changing behaviour is set before the main function runs.

import "github.com/spf13/viper"

func init() {
	// ...
	// optional environment variable name prefix
	viper.SetEnvPrefix("raker")
	// enabling the use of environment variables within viper
	viper.AutomaticEnv()
	// Setting name and file type for configuration file
	viper.SetConfigName(".raker")
	viper.SetConfigType("yaml")
	viper.AddConfigPath(".")
	// ...
}

Environment Variables #

From my experience, Viper can handle environment variables whose names maintain common practices while keeping the parsing/serialising code at minimum by explicitly binding variable names to Viper internal storage. In addition, since environment variables are an in-memory key-value store accessible by the shell and the running process, they cannot be mapped easily to a tree data structure implicitly. This means nested data structure properties must be explicitly bound to an environment variable using the same method.

import (
	"log"
	"os"
	"github.com/spf13/viper"
)

func init() {
	// ...
	viper.BindEnv("SECRET")
	viper.BindEnv("URI")
	viper.BindEnv("DATABASE")
	viper.BindEnv("STORAGE")
	viper.BindEnv("DIRECTORIES")
	viper.BindEnv("PORT")
	/*
	properties of nested structures can be set via environment variables by
	providing their before the to-be-bound environment variable's name
	*/
	viper.BindEnv("parent.child", "ENV_VAR")
	// ...
}

Invalid Values #

Viper’s parsing/serialising function supports type mismatches between the expected data structure and the inspected configuration state. However invalid values with values with valid types should be checked for after successful parsing/serialising. It might be possible to offload most of this validation to Viper by utilising its dependency’s custom data structure tags, but I haven’t looked into it yet.

package main

import (
	"log"
	"os"
	"github.com/spf13/viper"
)

func main() {
	// ...
	if err1 := viper.ReadInConfig(); err1 != nil {
		// configuration file not found errors should be handled differently for each application
		if _, err := os.Stat("/.dockerenv"); err != nil {
			/*
			if running in a Docker container, a missing configuration might prevent
			correct behaviour (depending on core functionality and lack of environment variable usage)
			*/
			log.Println(err1)
		}
	}

	if err := viper.Unmarshal(&configuration); err != nil {
		log.Fatal(err)
	}

	// checking for invalid empty strings for JWT secret
	if configuration.Secret == "" && !viper.IsSet("secret") {
		log.Fatal("A JWT secret must be set via a config file or an environment variable")
	}
	// ...
}