Managing configuration with Viper

Viper is a popular configuration library that’s designed with 12 factor applications in mind.

Viper is a complete configuration solution for go applications including 12 factor apps. It is designed to work within an application, and can handle all types of configuration needs and formats. Viper can be thought of as a registry for all of your applications configuration needs.

Let’s use it to provide configuration for a typical application.

Viper is available on github under spf13/viper. Start by vendoring viper and importing it for use in your application:

gvt fetch github.com/spf13/viper

You can create a viper instance by calling viper.New(). This allows you to manage configuration for multiple parts of a large application. If you don’t need it, you may use viper directly. Let’s create an utility function which will aid us to load configs and set default values.

func readConfig(filename string, defaults map[string]interface{}) (*viper.Viper, error) {
	v := viper.New()
	for key, value := range defaults {
		v.SetDefault(key, value)
	}
	v.SetConfigName(filename)
	v.AddConfigPath(".")
	v.AutomaticEnv()
	err := v.ReadInConfig()
	return v, err
}

Viper supports nested structures in the configuration, this is why the defaults are of the type map[string]interface{}. We loop through them and set default values for individual keys. Viper will use these after it checks that the value is not defined either in the configuration or the environment.

By calling SetConfigName we set the base filename where the config will be read from. Viper will search for supported formats by trying to open [filename].json, [filename].yaml and other supported formats. Viper will never read from [filename] directly, but will always append the extension for supported file types.

As we want to read the configuration from the current directory, we call AddConfigPath() with the parameter . (current directory). If you wanted to load configuration from multiple directories, you could repeat this call with different paths.

Finally, we call AutomaticEnv to ensure that Viper will read from environment variables as well. With this, we have everything we need to read any configuration format that Viper supports.

func main() {
  v1, err := readConfig(".env", map[string]interface{}{
    "port":     9090,
    "hostname": "localhost",
    "auth": map[string]string{
      "username": "titpetric",
      "password": "12fa",
    },
  })
  if err != nil {
    panic(fmt.Errorf("Error when reading config: %v\n", err))
  }

  port := v1.GetInt("port")
  hostname := v1.GetString("hostname")
  auth := v1.GetStringMapString("auth")

  fmt.Printf("Reading config for port = %d\n", port)
  fmt.Printf("Reading config for hostname = %s\n", hostname)
  fmt.Printf("Reading config for auth = %#v\n", auth)
}

We provide the default values with the second parameter to readConfig. As with the config files that Viper supports, nested structures are supported here as well. If we create a short .env.yaml file like this, viper will read from it nicely:

auth:
  username: black
  password: notthere

Keep in mind, if no config files are present, you will end up with an error. If you have an empty .env.json you will end up with an error as the contents are not a valid JSON document, you need at least {} to correctly parse JSON.

You can even read nested configuration structures with viper, by delimiting nested keys with . (dot). This means you can do v1.getString("auth.username") and get the value from the username key within the auth map. However, keep in mind that there’s no good way to override nested structures with environment variables at this point. You can do the following:

v1.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

And when you would provide the environment variable AUTH_USERNAME, you can retrieve it with v1.getString("auth.username"). However, if you will retrieve the full auth value as I did above, the nested value will not be updated with the value from the environment. As such, it doesn’t make much sense to rely on environment variables if you need nested structures. Viper could do a better job of importing those.

The full example is available in the 12FA with Doceker & Go book samples GitHub repo. There are two scripts provided which run the example with Docker, once with and once without environment variables. If you liked this article, consider buying a copy of 12 Factor applications with Docker & Go and consider signing up to be notified of new posts below.

About the author

I'm the author of API Foundations in Go and my latest book, 12 Factor Apps with Docker and Go. You should read both books if you want to learn more about Docker and Go. I write about technology, Docker, Go, Node, PHP and databases most of the time. I am always interested in implementing best practices in whatever I do. You can reach out to me on Twitter.

I'm available for consulting / development jobs. Fixing bottlenecks and scaling services to cope with high traffic is my thing. I specialize in back-end development with a focus on providing APIs for consumption in Angular, React, Vue.js and other front-end technology stacks.