How I do Go Application Configuration in 2020

August 28, 2020

There are many ways to handle application configuration in Go applications:

  1. Simple environment variable using os.Getenv("MY_AWESOME_ENV")

  2. Manually writing parsers for yaml/json/toml/hcl/envfile/java.properties files with their corresponding libraries.

  3. Reading in from an external system (e.g. etcd, consul, etc…) using their individual libraries/api’s.

  4. CLI flags

  5. Using a battle-harded library to do the hard stuff for you.

For this particular article, I’ll focus on number 5 from the list above because this is actually the way I do configuration these days.

Enter The Viper

So these days, rather than spending cycles maintaining my own configuration library, I use one of the more popular configuration libraries in the wild today:

https://github.com/spf13/viper

Viper allows you to do all of the common configuration scenarios and then some, so for my projects, it’s the best of all worlds.

Set it up

In all things Go these days, installing dependencies, Viper included, is as simple as running the following command in your “Go Modules” enabled project:

go get -u github.com/spf13/viper

Note: The “-u” in the go get command just updates the dependency if you already have it for some reason or another. I usually tend to include it.

Great! How do I use it?

The repository hosts a few good examples on usage, so feel free to check it out here: https://github.com/spf13/viper/blob/master/README.md

Considering that you are reading this particular article though, I’ll run through a few common scenarios that I find myself in when I write things these days.

The way I tend to really set this up is by defining a new Viper instance in a primary struct and having sub-components read from that configuration.

So let’s pretend that I want to build an HTTP server.

I typically will have a struct defined somewhere in my application that will look something like this:

...
type Server struct {
    // HTTPServer should be fairly obvious
    HTTPServer    *http.Server
    
    // Configuration is the Viper instance we will reference later on
    Configuration *viper.Viper

    // You may or may not need this, but most apps do :D
    Database      *sql.DB
}

func NewServer(cfg *viper.Viper) (*Server, error) {
    // my fancy server setup constructor logic
}
...

All of my handlers (db, web, etc…), hang off of this Server struct in some way or another, so these components can access the configuration data.

Scenario 1: Environment Variables

Viper has a few ways to work with environment variables. Personally, I like to be fairly explicit with what I am expecting, but Viper gives us the ability to simply set an environment variable prefix to handle things later:

...
viper.SetEnvPrefix("MYAPP_") // All environment variables that match this prefix will be loaded and can be referenced in the code
viper.AllowEmptyEnv(true) // Allows an environment variable to not exist and not blow up, I suggest using switch statements to handle these though
viper.AutomaticEnv() // Do the darn thing :D
...

Scenario 2: Configuration Files (regardless of extension!!)

Usually, you would support a single configuration file format and stick with that until the app dies or a new major version came out, but you don’t have to worry about that any more with Viper because it supports most of the big ones:

  • JSON
  • YAML
  • TOML
  • HCL
  • .env
  • .properties

You may be asking yourself, “But rando teacher internet guy, my config files are deeply nested! How do I access this data?”. Getting data is easy fortunately and just follows the dot-notion way of life:

...
viper.Get("my.deeply.nested.configuration.item")
...

this will get the value of the “item” item from the following JSON file:

{
  "my": {
    "deeply": {
      "nested": {
        "configuration": {
          "item": "check it"
        }
      }
    }
  }
}

Configuration items don’t have to be strings either, there are plenty of functions for pulling specific data types out:

  • GetBool
  • GetDuration
  • GetFloat64
  • GetInt
  • GetInt32
  • GetInt64
  • GetIntSlice
  • GetSizeInBytes
  • GetString
  • GetStringMap
  • GetStringMapString
  • GetStringMapStringSlice
  • GetStringSlice
  • GetTime
  • GetUint
  • GetUint32
  • GetUint64

The viper.Get() function just simply returns an interface{}, so if you can cast it, you can work with it.

To get this to work, all you have to do it provide a file name (without the file extension) and the :snake: magic just does it’s thing:

...
viper.SetConfigName("config")
...

If you really, really wanted to only support a particular config file type, you can do that too:

...
viper.SetConfigType("json")
...

You also can assign any number of other locations to look for this configuration file, the common ones I tend to use are something like the following:

...
viper.AddConfigPath(".")
viper.AddConfigPath("./config")
viper.AddConfigPath(path.Join(homeDirectory, ".my-awesome-app"))
viper.AddConfigPath(path.Join(homeDirectory, ".my-awesome-app/config"))
viper.AddConfigPath("/etc/my-awesome-app/")
viper.AddConfigPath("/etc/my-awesome-app/config")
...

Note: homeDirectory is a variable, so I have to set that somewhere :smile:

You can also set full config file location with:

...
viper.SetConfigFile("./config.yaml")
...

Or feel free to just provide this information via a CLI flag. Speaking of which…

Scenario 4: CLI Input

I’ll be real, I don’t do much CLI stuff anymore without Viper’s sister repo: {% github spf13/cobra no-readme %}

My only exception here is for something that maybe needs to use klog or some other very, very small app, but 9/10 times, Cobra all day.

The pair make it very, very easy to add CLI elements to existing text config files. There are just a few elements (such as maybe a –debug/–config flag) that just live better on the CLI only and not in a config file that is automatically consumed by your app.

Here is a quick snippet of what my CLI’s paired with Viper tend to look like:

...
func runCLI() {
    var (
        configFileFlag string

        vCfg = viper.New()

        myAppCmd = &cobra.Command{
            Use: "my-awesome-app",
            Version: myVersionConstant,
            Short: myShortDescriptionConstant,
            Long: myLongDescriptionConstant,
            Args: cobra.NoArgs(),
            PreRun: func(ccmd *cobra.Command, args []string{
                        vCfg.SetConfigFile(configFileFlag)
                        if err := vCfg.ReadInConfig(); err != nil {
                            log.Fatal("unable to read in config due to error: %+v", err)
                        }
                    },
            Run: func(ccmd *cobra.Command, args []string){
                     svr, err := server.New(vCfg)
                     if err != nil {
                         log.Fatalf("failed build server struct due to error: %+v", err)
                     }
                     if err := svr.Run(); err != nil {
                         log.Fatalf("server failed to start due to error: %+v", err)
                     }
                 },
        }
    )

    myAppCmd.Flags().StringVarP(&configFileFlag, "config", "f", "./config.yaml", "The path to the config file to use.")
    vCfg.BindPFlags("config", myAppCmd.Flags().Lookup("config"))

    if err := myAppCmd.Execute(); err != nil {
        log.Fatalf("the CLI failed to run due to error: %+v", err)
    }
}
...

Obviously, there is some crucial stuff missing from that, e.g. - my constant values, what my server.New(vCfg) function looks like, etc…, so to that end, I direct you to my article specific repository: https://gitlab.com/j4ng5y/how-i-write-go-configs-in-2020.git

Scenario 5: All of the above

I won’t go through the specific examples again :smile:, but please to check out my repository to see them all in action: https://gitlab.com/j4ng5y/how-i-write-go-configs-in-2020.git

Scenario 6: None of the above?

Yeah, that is right, you don’t really have to use any user modifiable way to do this and still have access to the same API that all your other apps use. You just have to set them up:

...
viper.SetDefault("MyConfigItem", "holla atcha boi")
...

Conclusion

The choice for configuration is ultimately yours, but I have found that using Viper has greatly simplified my application boot-straping.

This article was not to give you a fully thought out way of doing anything in particular, just sharing my experiences with the community as a whole.

If you have any further questions Re: this or anything else really for that matter, please don’t hesitate to comment here or reach out to me on twitter @j4ng5y.

You can find my on the Gopher slack @Jordan Gregory as well.


Want to hire me? E-mail Me!