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:
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 π 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 π
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:
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 π, 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.
Top comments (0)