DEV Community

Jordan Gregory
Jordan Gregory

Posted on • Edited on

How I Do Go Application Configuration in 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:

GitHub logo spf13 / viper

Go configuration with fangs

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
}
...
Enter fullscreen mode Exit fullscreen mode

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
...
Enter fullscreen mode Exit fullscreen mode

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")
...
Enter fullscreen mode Exit fullscreen mode

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

{
  "my": {
    "deeply": {
      "nested": {
        "configuration": {
          "item": "check it"
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 🐍 magic just does it's thing:

...
viper.SetConfigName("config")
...
Enter fullscreen mode Exit fullscreen mode

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

...
viper.SetConfigType("json")
...
Enter fullscreen mode Exit fullscreen mode

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")
...
Enter fullscreen mode Exit fullscreen mode

Note: homeDirectory is a variable, so I have to set that somewhere πŸ˜„

You can also set full config file location with:

...
viper.SetConfigFile("./config.yaml")
...
Enter fullscreen mode Exit fullscreen mode

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 logo spf13 / cobra

A Commander for modern Go CLI interactions

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)
    }
}
...
Enter fullscreen mode Exit fullscreen mode

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 πŸ˜„, 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")
...
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)