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.
While I have you here...
It would be great if you buy one of my books:
- Go with Databases
- Advent of Go Microservices
- API Foundations in Go
- 12 Factor Apps with Docker and Go
Feel free to send me an email if you want to book my time for consultancy/freelance services. I'm great at APIs, Go, Docker, VueJS and scaling services, among many other things.
Want to stay up to date with new posts?
Stay up to date with new posts about Docker, Go, JavaScript and my thoughts on Technology. I post about twice per month, and notify you when I post. You can also follow me on my Twitter if you prefer.