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.

Appendix