Introduction
This is a quick reference guide on what the spf13/viper package is, and how it can be used to help speed up your development.
As a prerequisite, you'll need to be able to read Golang.
As with previous primers, this does not replace documentation and is aimed at being just a cheatsheet or quickstart resource!
A note that the developer spf13 is leaving their role as Product Lead for Go at google. Thanks for everything, Steve!
What is Viper
Taken directly from Viper's documentation:
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.
Or put simply, Viper is a library that parses and manages configuration language so you don't have to! Spend less time writing code to manage your configuration and instead focus on your business logic!
Reading Simple Configuration
Say we've got an application that has a simple configuration file that looks like this:
logLevel: debug
magicFeatureEnabled: true
somethingNeatEnabled: false
That's a pretty simple YAML file that we can easily represent as a Golang struct like so:
type Configuration struct {
LogLevel string `yaml:"logLevel"`
MagicFeatureEnabled bool `yaml:"magicFeatureEnabled"`
SomethingNeatEnabled bool `yaml:"somethingNeatEnabled"`
}
And these struct tags on each key of the Configuration struct are read by tools like the go-yaml library to map the data in our YAML file to the struct in our code. Reading this file is as easy as this:
package main
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
)
func main() {
d, _ := os.ReadFile("config.yaml")
var cfg Configuration
yaml.Unmarshal(d, &cfg)
fmt.Println("The Log Level is: ", cfg.LogLevel)
fmt.Println("We are using the Magic Feature: ", cfg.MagicFeatureEnabled)
fmt.Println("We are using Something Neat ", cfg.SomethingNeatEnabled)
}
type Configuration struct {
LogLevel string `yaml:"logLevel"`
MagicFeatureEnabled bool `yaml:"magicFeatureEnabled"`
SomethingNeatEnabled bool `yaml:"somethingNeatEnabled"`
}
Running this with our previous configuration file gives us this result:
The Log Level is: debug
We are using the Magic Feature: true
We are using Something Neat false
So far so good.
Reading Complex Configuration
So we've already got a simple way to read in a file from disk and map it to a struct in Go, allowing us to make decisions based on the user's configuration.
What if we want the user to be able to pass us JSON instead of YAML? Well that's not too bad. We just add a few more struct tags like this:
type Configuration struct {
LogLevel string `yaml:"logLevel" json:"loglevel"`
MagicFeatureEnabled bool `yaml:"magicFeatureEnabled" json:"magicFeatureEnabled"`
SomethingNeatEnabled bool `yaml:"somethingNeatEnabled" json:"somethingNeatEnabled"`
}
And then we can use the encoding/json
library which also has an Unmarshal function. Reading a file from disk has
already been done, so we'd just need to read the file at both a config.json or
config.yaml path and then repeat the process as we did with the YAML
configuration.
Easy peasy.
Accepting configuration through environment
These days, environment variables are all the rage. They're easy to change in continuous integration, test environments, etc. So let's add an environment variable for each of these... something like:
const (
EnvLogLevel = "MYAPP_LOG_LEVEL"
EnvMagicFeatureEnabled = "MYAPP_MAGIC_FEATURE_ENABLED"
EnvSomethingNeatEnabled = "MYAPP_SOMETHING_NEAT_ENABLED"
)
So these are our environment variable keys, and so we need to look for each of them. In our example application, we'll enforce that environment variables override the configuration file. Establishing and documenting precedence is important, so that users know exactly how and why a value is being overridden!
So now our main.go has to handle the precedence, so we'll read the
configuration first, and then we'll read the environment.
package main
import (
"fmt"
"os"
"strings"
"gopkg.in/yaml.v3"
)
const (
EnvLogLevel = "MYAPP_LOG_LEVEL"
EnvMagicFeatureEnabled = "MYAPP_MAGIC_FEATURE_ENABLED"
EnvSomethingNeatEnabled = "MYAPP_SOMETHING_NEAT_ENABLED"
)
func main() {
// read the configuration file from disk and store it in our struct.
d, _ := os.ReadFile("config.yaml")
var cfg Configuration
yaml.Unmarshal(d, &cfg)
// override the configuration with environment variables
if key := os.Getenv(EnvLogLevel); key != "" {
cfg.LogLevel = key
}
if key := os.Getenv(EnvMagicFeatureEnabled); key != "" {
envValue := false
if strings.ToLower(key) == "true" {
envValue = true
}
cfg.MagicFeatureEnabled = envValue
}
if key := os.Getenv(EnvSomethingNeatEnabled); key != "" {
envValue := false
if strings.ToLower(key) == "true" {
envValue = true
}
cfg.SomethingNeatEnabled = envValue
}
// run our business logic
fmt.Println("The Log Level is: ", cfg.LogLevel)
fmt.Println("We are using the Magic Feature: ", cfg.MagicFeatureEnabled)
fmt.Println("We are using Something Neat ", cfg.SomethingNeatEnabled)
}
type Configuration struct {
LogLevel string `yaml:"logLevel" json:"loglevel"`
MagicFeatureEnabled bool `yaml:"magicFeatureEnabled" json:"magicFeatureEnabled"`
SomethingNeatEnabled bool `yaml:"somethingNeatEnabled" json:"somethingNeatEnabled"`
}
Without modifying the environment, our output looks like this (values from our configuration file):
$ go run .
The Log Level is: debug
We are using the Magic Feature: true
We are using Something Neat false
But if we modify our environment, we see that our environment's values win out over the configuration file.
$ MYAPP_LOG_LEVEL=info go run .
The Log Level is: info
We are using the Magic Feature: true
We are using Something Neat false
So far, things have been pretty simple! But more than half of our main.go file is focused on handling our configuration... and we haven't even scratched the surface!
What if we wanted to have more complex configurations, with maps or arrays?
logging:
level: debug
file: /var/log/myapp.log
magicFeature:
enabled: true
backends:
- awesome.example.com:8080
- epic.example.com:8080
somethingNeat:
enabled: true
endpoint: https://neat.example.com/v1/
Building out a struct in Go is simple enough, but now each of these keys needs a corresponding environment variable. And for each environment variable, we'll need to build out logic that replaces the values read in from our config file with the values read in from the environment.
What about default values? If the user didn't provide a value, but we need it to
run our logic, it stands to reason that a sane default should be used. We'll
need to codify that default either by building out a DefaultConfig out of our
Configuration struct, reading a default from disk, or really any other way
that you can architect your solution.
None of these problems are too complex to solve on your own, but the Viper library aims to help speed up your development by giving you simple hooks that solve this problem without having to write your own code to do so.
Hands on with Viper
Viper is a library, and as such, we just need to import it or go get the
module into our application. For this cheat sheet, we're mostly interested in
the Viper library calls and so we won't really do much business logic other than
just printing out our values. If you want to follow along, create a directory
called configprinter and install Viper into this path.
# create the directory and change into it
mkdir configprinter
cd configprinter
# create an empty main.go
echo package main >> main.go
# initialize the module and install viper
go mod init example.com/configprinter
go get github.com/spf13/viper
go mod tidy
Then open up main.go.
NOTE: Viper can be very tightly integrated with Cobra. We'll look at that later.
Defining our Config File
You should have main.go open at this point with just the package main
declaration. To get started, we'll add the Viper import as well as the fmt
package and a basic main funcion that does nothing.
package main
import (
"fmt"
"github.com/spf13/viper"
)
func main() {
fmt.Println("Start")
defer fmt.Println("end")
}
The very first thing we'll do is define that we want our users to be able to define a YAML config file in the running directory. For that, we'll use three functions from the Cobra library
package main
import (
// ... unchanged ...
)
func main() {
// ... unchanged ...
viper.AddConfigPath(".")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
if err := viper.ReadInConfig(); err != nil {
// handle this error if you desire
fmt.Println("config file not found")
}
}
And at this point, running your code would just print out the start/end messages and a message indicating that a config file was not found. Simple enough.
$ go run .
Start
config file not found
end
Create a config file with the name config.yaml. Leave it empty, and then run program again.
touch config.yaml
The application should stop complaining about not having a configuration file. Even with no values, the file exists and it has the right extension (.yaml) so we know that this file is being read by our program.
$ go run .
Start
end
Reading Config Values
At this point, we've already defined that the user can put in configuration into a file config.yaml and the application will read it. But what config values is it reading?
When using viper, access to your configuration values is pretty free-form, and doesn't require the use of any structs or defined types in your code.
Let's add the logLevel key to our configuration.
echo logLevel: debug >> config.yaml
And at the very bottom of our main() function, we'll add a print statement
that queries from Viper the value of our logLevel key.
package main
// ... unchanged ...
func main() {
// ... unchanged ...
fmt.Println("The log level is set to:", viper.GetString("logLevel"))
}
Running the command shows you the same value for logLevel as is in our configuration.
$ go run .
Start
The log level is set to: debug
end
And so you really only need to know the type of the value in your
configuration. This is because Go is strongly typed, but YAML has limited types
that can be represented in its markup. There are equivalent Get<Type> functions
for the various types that you can define in Golang.
It's important to note that if you use the wrong type getter function, you
get the empty value for that type, as viper.Get<Type> functions do not
return errors, or panic. So for example, running viper.GetBool for logLevel
would give us false. Running viper.GetInt would give us zero, etc.
As a practice: add another key to your config enableLogging with value set to
true, and get and print out its value. You can decide what the accompanying message says.
$ go run .
Start
The log level is set to: false
logging is enabled: true
end
Also, feel free to remove the Start and End statements at this point now that we have something else printing out for us.
What's actually happening
Before we go on, I want to point out that what's happening here isn't magic.
The Viper library makes a Viper type, which itself is responsible for implementing all of the logic we've called so far.
That includes:
-
setting our configfile name to
config -
setting our extension to
yaml -
setting the path to
.or "my current directory" -
reading in the configuration file at the the above path
-
getting values from the configuration file read from that path
But we didn't create any instances of Viper! That's because the viper library
itself provides a "global" or "singleton" instance of the Viper struct (seen
here) This is purely
out of convenience to callers, and it allows developers to simply import the
library, tell the library where the configuration file is, and be on their way
writing business logic
If you wanted to create your own instance of a Viper struct, you can
absolutely use the viper.New() function to instantiate one and use it
standalone. For the purpose of this text, however, we'll use the singleton.
Now, back on topic!
The init function
So now that we know a little bit about what's happening under the hood, we're going to change things around just a little bit more before we get back into Viper's features.
In Go, the init function is a special
function that allows you to establish any kind of state you need to manage for
your code to operate before your main function is executed. The link above
describes in more detail the exact semantics of when the init() function runs,
if you're interested.
We currently have our Viper configuration taking place in our main() function,
but it's more common to have this take place in an init() function, or
something init-adjacent (like another function that is called by a package's
init function). Because Viper tends to represent the global configuration of
an application, it's fairly common to include its configuration in the init
function of your main.go, or as close to your entrpoint as possible. That
ensures that configuration parsing always happens when your entrypoint is
called.
Let's move most of our Viper-related function calls over to the init function
before we go further.
package main
import // ... unchanged ...
func main() {
if err := viper.ReadInConfig(); err != nil {
// handle this error if you desire
fmt.Println("config file not found")
}
fmt.Println("The log level is set to:", viper.GetBool("logLevel"))
fmt.Println("logging is enabled:", viper.GetBool("enableLogging"))
}
func init() {
viper.AddConfigPath(".")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
}
You might notice that I didn't move the viper.ReadInConfig call into the init
function. You absolutely can do this as well - just make sure to make it the
last thing called. For this example, I've left it in main as it makes things
easier to represent.
Now back to the config hacking!
Maps and Slices
One of the huge benefits of using a tool like Viper is the ability to use dot notation to get various config values in your tree. So let's extend our config.yaml to look like this:
logLevel: debug
enableLogging: true
metrics:
listenAddress: 127.0.0.1
listenPort: 9999
backends:
- 192.168.10.01:8001
- 192.168.10.01:8002
- 192.168.10.01:8003
- 192.168.10.01:8004
Now we've got a metrics entry with various keys, and a backends array with
various entries. And we can access them using Viper getter functions.
The metrics end effectively is an "object", or a "map" in Go terms. Accessing
the listenAddress key can be done using dot notation, similar to what you might
see using tools like jq or yq.
func main() {
// ... unchanged ...
fmt.Println("The metrics endpoint is:",
fmt.Sprintf("%s:%s",
viper.GetString("metrics.listenAddress"),
viper.GetString("metrics.listenPort"),
),
)
}
An item in the backends key can be accessed the same way:
func main() {
// ... unchanged ...
fmt.Println("The first backend is: ", viper.GetString("backends.0"))
}
But what if you need the entire slice of backends? There's a getter for that, too:
func main() {
// ... unchanged ...
fmt.Println("Here are all the backends:", viper.GetStringSlice("backends"))
}
After adding all of these, here's what we see in our output:
The log level is set to: false
logging is enabled: true
The metrics endpoint is: 127.0.0.1:9999
The first backend is: 192.168.10.01:8001
Here are all the backends: [192.168.10.01:8001 192.168.10.01:8002 192.168.10.01:8003 192.168.10.01:8004]
Adding Environment Variables
One of the more painful things in our introductory example was handling environment variables, and I promised Viper would make this easier.
Viper actually has a feature called AutomaticEnv which will automatically read
your configuration keys and look for a standardized environment variable string
that matches.
The docs go
into more detail about the semantics of working with environment variables, but
suffice to say that AutomaticEnv will take a key logLevel and look for the
equivalent LOGLEVEL from the environment.
Let's enable AutomaticEnv by adding it to our init() function.
func init() {
viper.AddConfigPath(".")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AutomaticEnv()
}
The run your code while overridding the debug value for LOGLEVEL with info.
$ LOGLEVEL=info go run .
The log level is set to: info
However, it's possible (and almost likely) for another application to also
expect the LOGLEVEL environment variable, so you can add a prefix to your
environment variables to disambiguate them from others.
func init() {
// ... unchanged ...
viper.SetEnvPrefix("cp")
viper.AutomaticEnv()
}
Now our environment variables must have the CP_ prefix, e.g. CP_LOGLEVEL.
$ CP_LOGLEVEL=info go run .
The log level is set to: info
...
Without the prefix, the value is ignored.
$ LOGLEVEL=info go run .
The log level is set to: debug
...
Setting Nested Keys via Environment
Setting a nested key via the environment takes another configuration option. Accessing nested keys in our configuration cannot be done in most shells using dot notation, so we just tell Viper to replace any paths that contain a dot with an underscore.
func init() {
// ... unchanged ...
viper.SetEnvPrefix("CP")
viper.SetEnvKeyReplacer(strings.NewReplacer(`.`, `_`))
viper.AutomaticEnv()
}
Now, the metrics.listenAddress path can be accessed at the environment. Don't forget the prefix!
$ CP_METRICS_LISTENADDRESS=localhost go run .
The log level is set to: debug
logging is enabled: true
The metrics endpoint is: localhost:9999 # this changed!
The first backend is: 192.168.10.01:8001
Here are all the backends: [192.168.10.01:8001 192.168.10.01:8002 192.168.10.01:8003 192.168.10.01:loca8004]
You can also change the backends array. Just separate your items with spaces:
$ CP_BACKENDS="10.0.0.1:8001 10.0.0.1:8002" go run .
The log level is set to: debug
logging is enabled: true
The metrics endpoint is: 127.0.0.1:9999
The first backend is:
Here are all the backends: [10.0.0.1:8001 10.0.0.1:8002] # two new items!
Binding to structs
So while it's not strictly required, you can optionally bind your entire
configuration, or even just pieces of it, to golang struct types. Say we had a
new type MetricsConfig with our metrics-related keys.
type MetricsConfig struct {
ListenAddress string
ListenPort string
}
We can read (or "Unmarshal") the metrics key from our config directly into this struct:
func main() {
// ... unchanged ...
var metricsConf MetricsConfig
viper.UnmarshalKey("metrics", &metricsConf)
fmt.Println("metricsConf ListenAddress", metricsConf.ListenAddress)
fmt.Println("metricsConf ListenPort", metricsConf.ListenPort)
}
Your struct then contains the configuration values for use in whatever functions you might have.
$ go run .
# ... previous output omitted ...
metricsConf ListenAddress 127.0.0.1
metricsConf ListenPort 9999
There is also an Unmarshal functionthat allows you to bind the entire
configuration to a struct, should you need it
Reading configuration across packages
One of the nicer features of having Viper manage your configuration in its own singleton is that you can read your configuration across packages, so long as you've already executed your viper initialization when you work with your other packages.
So as an example, I'll create a new directory (to represent a new package, or
module) called backends, and I'll write a small function that enumerates my
backends.
I'm running this from my project's base directory.
mkdir backends
echo package backends >> backends/backends.go
I have this sample function Enumerate. The logic isn't super important.
package backends
import (
"fmt"
"github.com/spf13/viper"
)
func Enumerate() {
fmt.Println("Hello from the backends package!")
backends := viper.GetStringSlice("backends")
for i, backend := range backends {
fmt.Printf("%d: %s\n", i, backend)
}
}
What is important is that you notice that I've simply
called viper.GetStringSlice directly. Access to configuration entries is
vastly simplified using tools like Viper. This is also made possible because
we've leveraged our main.go's init function to configure Viper, and that
doing so causes that configuration to take place well before any of this code is
called.
Add this to your main function and see that it works as expected.
func main() {
// ... unchanged ...
backends.Enumerate()
}
And the result is as expected!
$ go run .
# ... unchanged ...
Hello from the backends package!
0: 192.168.10.01:8001
1: 192.168.10.01:8002
2: 192.168.10.01:8003
3: 192.168.10.01:8004
Setting Defaults
You'll eventually need to set a default value for a configuration item, and doing so is a single line away.
Let's define an os key to our code. Don't add it to your config.yaml!
func main() {
// ... unchanged ...
fmt.Println("The os is", viper.GetString("os"))
}
func init() {
// ... unchanged ...
viper.SetDefault("os", "centos")
}
Run the code and see that the default value is returned:
$ go run .
# ... unchanged ...
The os is centos
Setting Values
As we've seen already, Viper (when used with the library's singleton) can easily be used to access configuration across packages. What if a value needs to be changed after reading the configuration due to a change in state internally in your application?
Viper also provides a setter (e.g. viper.Set) to do exactly that. Let's set a key workers.
func main() {
// ... unchanged ...
fmt.Println("the worker count is", viper.GetInt("workers"))
fmt.Println("setting workers to 4")
viper.Set("workers", 4)
fmt.Println("the worker count is:", viper.GetInt("workers"))
}
This produces:
$ go run .
# ... unchanged ...
the worker count is 0
setting workers to 4
the worker count is: 4
Now the key workers can be accessed from any other package by accessing the
value from Viper.
Reading other config files
It stands to reason that you may want to allow for multiple locations where a
configuration file might be found. An example might be if your application is
installed via a linux distribution's package manager, and its configuration file
is placed inside the /etc directory.
We've only told Viper to search relative to our current directory (i.e. .). So
let's also add /etc/configprinter and also the local ./conf directory.
func init() {
viper.AddConfigPath("/etc/configprinter/")
viper.AddConfigPath("./conf")
viper.AddConfigPath(".")
// ... unchanged ...
}
Move your configuration to any of these locations and Viper should still find your config file.
Reading other config formats
A given instance of Viper can only read a single configuration format (e.g.
YAML), but Viper can read various configuration formats.
We've told Viper directly that our configuration file would be an the YAML format by calling this function:
viper.SetConfigType("yaml")
This actually isn't strictly required. What this does is it allows us to call
our configuration file simply config with no extension. It tells Viper exactly
what format to expect.
If you plan on using file extensions for your configuration file, then you can
simply leave this directive out. Then Viper will look for the file config
(which we defined) at any of the configured locations with common file
extensions for its various supported formats.
Here's the same configuration, but as a TOML file:
backends = ['192.168.10.01:8001', '192.168.10.01:8002', '192.168.10.01:8003', '192.168.10.01:8004']
enablelogging = true
loglevel = 'debug'
os = 'centos'
workers = 4
[metrics]
listenaddress = '127.0.0.1'
listenport = 9999
Delete the config.yaml file you have and replace it with this contents in a file called config.toml.
Then, remove the viper.SetConfigType call and rerun the code. Everything should still be in place.
$ go run .
The log level is set to: debug
logging is enabled: true
The metrics endpoint is: 127.0.0.1:9999
The first backend is: 192.168.10.01:8001
Here are all the backends: [192.168.10.01:8001 192.168.10.01:8002 192.168.10.01:8003 192.168.10.01:8004]
metricsConf ListenAddress 127.0.0.1
metricsConf ListenPort 9999
Hello from the backends package!
0: 192.168.10.01:8001
1: 192.168.10.01:8002
2: 192.168.10.01:8003
3: 192.168.10.01:8004
The os is centos
the worker count is 4
setting workers to 4
the worker count is: 4
By the way, need a quick way to convert your config to a different format? Try setting:
viper.SetConfigType("yourformat")
viper.SafeWriteConfigAs("path")
The list of config types supported by Viper are found here.
Integrating With Cobra
Note that this section leaves our previous configprinter project behind. The
referenced code is in the code/snakes directory of this
repository.
Cobra and viper work really well together. When building commandline interfaces with cobra, users can specify flags to indicate behavioral changes in their program, and Viper can receive those values and store them in configuration.
For this, I've scaffolded a new project using cobra-cli. When calling
cobra-cli, make sure to use the --viper flag to scaffold out the Viper
integrations.
$ cobra-cli init --viper=true
Your Cobra application is ready at
/Users/me/.go/src/github.com/opdev/viper-primer/code/snakes
As expected, we get some scaffolded code, and most importantly, we get our cmd/root.go
$ tree
.
├── LICENSE
├── cmd
│ └── root.go
├── go.mod
├── go.sum
└── main.go
Typically, the rootCmd is empty and is an organizational command (like calling
cobra-cli with no subcommands), but for this example, we'll make our changes
there so we don't have to scaffold out extra subcommands.
If you've gone through the Cobra
Primer, you'll notice a few differences
in the cmd/root.go that's scaffolded when Viper support is enabled for your
application.
Notably, you'll see the init() function now calls an initConfig function.
And that initConfig function initializes a basic Viper configuration similar
to what we did in the configprinter example project. Here's what we get (a
few comments and blocks have been omitted).
package cmd
// imports omitted
var cfgFile string
// ... cobra code omitted
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.snakes.yaml)")
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := os.UserHomeDir()
cobra.CheckErr(err)
// Search config in home directory with name ".snakes" (without extension).
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".snakes")
}
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}
Reading the initConfig, code, we'll see that that the user can set a config
file path, which is stored in the variable cfgFile, via a flag. If they do,
that's what's used by Viper. Alternatively, we will check $HOME/.snakes.yaml
for a configuration file.
Finally, viper.AutomaticEnv() is called, meaning that we can override values
using environment variables. Finally, ReadInConfig is called, and so the Viper
configuration file is read.
Uncomment the rootCmd.Run struct key and just fill in some placeholder logic
so that we can execute it without getting help output. I've also removed the
Long and Short descriptions just so that this text looks cleaner:
var rootCmd = &cobra.Command{
Use: "snakes",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Run executed")
},
}
At this point, we can run our command and get our demo print statements:
$ go run .
Run executed
cobra-cli scaffolded a --toggle boolean flag for us. Let's add another line
to print the value of that flag.
// unchanged
var rootCmd = &cobra.Command{
Use: "snakes",
Short: "A brief description of your application",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Run executed")
toggleValue, _ := cmd.Flags().GetBool("toggle")
fmt.Println("Toggle is set to: ", toggleValue)
},
}
// unchanged
So if we run this now, we see the toggle value printed (it has a default value of false). We can set it to true by running something like the following:
$ go run . --toggle
Run executed
The toggle flag is set to: true
But right now, this value never makes it to our Viper configuration. Try to access to toggle key using viper, and you'll see that regardless of whether the user runs the command with the --toggle flag, Viper never sees the change.
// unchanged
var rootCmd = &cobra.Command{
Use: "snakes",
Short: "A brief description of your application",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Run executed")
toggleValue, _ := cmd.Flags().GetBool("toggle")
fmt.Println("The toggle flag is set to: ", toggleValue)
fmt.Println("The toggle config in viper is set to:", viper.GetBool("toggle"))
},
}
// unchanged
And here's what we see when we run this with the --toggle flag:
$ go run . --toggle
Run executed
The toggle flag is set to: true
The toggle config in viper is set to: false
Right now, flag values aren't being stored in our Viper configuration. Luckily, Viper provides a way to bind the flag's value to the configuration.
In our init function, where we define the rootCmd's flag(s), we can also
bind an equivalent Viper configuration value:
func init() {
// ... unchanged ...
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
viper.BindPFlag("toggle", rootCmd.Flags().Lookup("toggle"))
}
Note that technically we can bind another Viper configuration key to the flag's key, but mapping the values similarly is common.
Now, we see our Viper configuration contains the value of our flag.
$ go run . --toggle
Run executed
The toggle flag is set to: true
The toggle config in viper is set to: true
$ go run .
Run executed
The toggle flag is set to: false
The toggle config in viper is set to: false
Accessing configuration once you've integrated Cobra and Viper
Once you've bound your Viper configuration to Cobra, you can technically access your user's configuration using either. With that said, it (subjectively) makes sense to leverage your Viper configuration as your source of truth once your cobra flags are bound. This is because you can store the values of your flags in your Viper configuration, but it's not exactly a bi-directional relationship, and your Viper configuration values don't get stored in your cobra flags.
To that end, if you're going to leverage viper, I would probably aim to use it as your source of truth.
A note on case discrepancies between viper and cobra flags.
Let's add a --log-level string flag, and corresponding Viper binding:
func init() {
// ... unchanged ...
rootCmd.Flags().StringP("log-level", "l", "", "Help message for log level")
viper.BindPFlag("logLevel", rootCmd.Flags().Lookup("log-level"))
}`
Viper configurations are typically configured using camelCase or snake_case, but long flags are typically hyphenated. You'll likely want to bind your flag values to appropriate Viper configuration values in cases where you have hyphenated long flags by binding them to equivalent values in your preferred Viper-friendly case.
Here, I've decided to use camelCase. If the user provides a configuration file,
the key for log level would need to be logLevel.
A note on precedence
It's important to understand the precedence of your configuration, especially when you integrate with Cobra and mix flags in. I won't document it here, but it's documented in the Viper documentation:
https://github.com/spf13/viper#why-viper
A note on flag integration
While we demonstrate that Cobra and Viper play nicely together, it's also possible to integrate with the flag and pflag packages in Golang directly.
Final Thoughts
Each library that you use should save you (as a developer) time worrying about a problem that's already solved, and should free you up to focus on your own business logic. Hopefuly the Viper library helps you manage your configuration flexibly so that you can move that much more quickly.