Locating Configuration Files

In this lesson, we'll take an in deep look at Viper's support for configuration files.

We'll cover the following

Configuration files are a staple of program configuration, especially when the configuration data is complicated and hierarchical in nature. Viper supports a large number of configuration files, including JSON, TOML, YAML, HCL, INI, envfile and Java Properties files. In this lesson, you will see how to locate the configuration file. Later, we will see how to write configuration files, how to read them and even how to watch them for changes.

Overview

When working with configuration files there are several tasks. First, you need to locate where the configuration file is, and then you need to read and parse its contents. Under some circumstances, you may also want to write or update a configuration file from your program. For more dynamic situations, it’s important to be able to watch a configuration file for changes and re-read it to reflect the configuration changes. Let’s see how Viper allows you to perform all these tasks.

Locating configuration files

Viper doesn’t make any assumptions about your configuration file or its location. There is no default search path or a default configuration filename. This means that if we want to work with a configuration file, we have to tell Viper how to find it. The Viper model for locating configuration file consists of three elements:

  • The search path
  • The config name
  • The file type

The search path is a list of directories where Viper will check to see if the configuration file is present. We can add directories to the search path using the AddConfigPath() function

package main

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

func main() {
	viper.AddConfigPath(".")
	viper.AddConfigPath("~/test-config")
	viper.AddConfigPath("/no-such-dir")
	viper.AddConfigPath("/tmp/test-config")
}

You must add at least one directory for Viper to find your configuration file. If you don’t specify any search paths, you must provide a path to a configuration file.

Once the search path is established, we need to tell Viper what configuration file to look for. Again, there is no default. However, since Viper can read configuration files in multiple formats, you can leave out the file extension and just provide base filename:

func main() {
	viper.AddConfigPath(".")
	viper.AddConfigPath("~/test-config")
	viper.AddConfigPath("/no-such-dir")
	viper.AddConfigPath("/tmp/test-config")

	viper.SetConfigName("config")

Viper will start searching the directories for files with a base name of “config” and an extension, which is one of the supported extensions. What happens if more than one file is matching? Viper will return to the first match.

I checked the Viper source code to see what the order of precedence is between the supported formats. This is an implementation detail, but here it is if you’re curious:

// SupportedExts are universally supported extensions.
var SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "dotenv", "env", "ini"}

Note that the directories are also searched in order too, so if there is config.toml in the current directory, which is the first directory in the search path then, it takes precedence over config.json in “~/test-config”, which is the second directory. The file extensions precedence is important only for files in the same directory.

Sometimes, you may use a file with a non-standard extension or no extension at all. For example, suppose you have a config file called “Configfile”. You can tell Viper explicitly the name of the configuration file and its type if it can’t infer it from the file extension:

func main() {
	viper.AddConfigPath(".")
	viper.AddConfigPath("~/test-config")
	viper.AddConfigPath("/no-such-dir")
	viper.AddConfigPath("/tmp/test-config")
	
	viper.SetConfigFile("Configfile")
	viper.SetConfigType("toml")
}

If you explicitly specify a filename with a supported extension there is no need to specify the type:

func main() {	
	viper.SetConfigFile("Configfile.toml")
}

In all these examples, the search path and the name of the configuration file were hardcoded in the program itself. It is very common for the users to specify different configuration files when invoking the program. In this case the configuration filename can be provided as an environment variable or a command-line flag. Here is an example of, of a configuration file provided as an environment variable.

	viper.SetEnvPrefix("VIPER")
	err := viper.BindEnv("CONFIG")
	if err != nil {
		fmt.Println(err.Error())
		return
	}
	configFile := viper.GetString("config")
	viper.SetConfigFile(configFile)

Try running the app below to set the configuration path and files.

Get hands-on with 1200+ tech skills courses.