Go Configuration Manager Based on Viper

What is Viper?
Viper is a complete configuration solution
for go applications including 12 factor apps.
What is configman?
TODO
Putting Values into Viper
Establishing Defaults
A good configuration system will support default values. A default value is not
required for a key, but it's useful in the event that a key hasn’t been set via
config file, environment variable, remote configuration or flag.
Examples:
viper.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})
Watching and re-reading config files
Viper supports the ability to have your application live read a config file while running.
Gone are the days of needing to restart a server to have a config take effect,
viper powered applications can read an update to a config file while running and
not miss a beat.
Simply tell the viper instance to watchConfig.
Optionally you can provide a function for Viper to run each time a change occurs.
Make sure you add all of the configPaths prior to calling WatchConfig()
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
})
Reading Config from io.Reader
Viper predefines many configuration sources such as files, environment
variables, flags, and remote K/V store, but you are not bound to them. You can
also implement your own required configuration source and feed it to viper.
viper.SetConfigType("yaml") // or viper.SetConfigType("YAML")
// any approach to require this configuration into your program.
var yamlExample = []byte(`
Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
jacket: leather
trousers: denim
age: 35
eyes : brown
beard: true
`)
viper.ReadConfig(bytes.NewBuffer(yamlExample))
viper.Get("name") // this would be "steve"
Setting Overrides
These could be from a command line flag, or from your own application logic.
viper.Set("Verbose", true)
viper.Set("LogFile", LogFile)
Registering and Using Aliases
Aliases permit a single value to be referenced by multiple keys
viper.RegisterAlias("loud", "Verbose")
viper.Set("verbose", true) // same result as next line
viper.Set("loud", true) // same result as prior line
viper.GetBool("loud") // true
viper.GetBool("verbose") // true
Working with Environment Variables
Viper has full support for environment variables. This enables 12 factor
applications out of the box. There are four methods that exist to aid working
with ENV:
AutomaticEnv()
BindEnv(string...) : error
SetEnvPrefix(string)
SetEnvReplacer(string...) *strings.Replacer
When working with ENV variables, it’s important to recognize that Viper
treats ENV variables as case sensitive.
Viper provides a mechanism to try to ensure that ENV variables are unique. By
using SetEnvPrefix, you can tell Viper to use add a prefix while reading from
the environment variables. Both BindEnv and AutomaticEnv will use this
prefix.
BindEnv takes one or two parameters. The first parameter is the key name, the
second is the name of the environment variable. The name of the environment
variable is case sensitive. If the ENV variable name is not provided, then
Viper will automatically assume that the key name matches the ENV variable name,
but the ENV variable is IN ALL CAPS. When you explicitly provide the ENV
variable name, it does not automatically add the prefix.
One important thing to recognize when working with ENV variables is that the
value will be read each time it is accessed. Viper does not fix the value when
the BindEnv is called.
AutomaticEnv is a powerful helper especially when combined with
SetEnvPrefix. When called, Viper will check for an environment variable any
time a viper.Get request is made. It will apply the following rules. It will
check for a environment variable with a name matching the key uppercased and
prefixed with the EnvPrefix if set.
SetEnvReplacer allows you to use a strings.Replacer object to rewrite Env
keys to an extent. This is useful if you want to use - or something in your
Get() calls, but want your environmental variables to use _ delimiters. An
example of using it can be found in viper_test.go.
Env example
SetEnvPrefix("spf") // will be uppercased automatically
BindEnv("id")
os.Setenv("SPF_ID", "13") // typically done outside of the app
id := Get("id") // 13
Working with Flags
Viper has the ability to bind to flags. Specifically, Viper supports Pflags
as used in the Cobra library.
Like BindEnv, the value is not set when the binding method is called, but when
it is accessed. This means you can bind as early as you want, even in an
init() function.
The BindPFlag() method provides this functionality.
Example:
serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))
The use of pflag in Viper does not preclude
the use of other packages that use the flag
package from the standard library. The pflag package can handle the flags
defined for the flag package by importing these flags. This is accomplished
by a calling a convenience function provided by the pflag package called
AddGoFlagSet().
Example:
package main
import (
"flag"
"github.com/spf13/pflag"
)
func main() {
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
...
}
Flag interfaces
Viper provides two Go interfaces to bind other flag systems if you don't use Pflags.
FlagValue represents a single flag. This is a very simple example on how to implement this interface:
type myFlag struct {}
func (f myFlag) HasChanged() bool { return false }
func (f myFlag) Name() string { return "my-flag-name" }
func (f myFlag) ValueString() string { return "my-flag-value" }
func (f myFlag) ValueType() string { return "string" }
Once your flag implements this interface, you can simply tell Viper to bind it:
viper.BindFlagValue("my-flag-name", myFlag{})
FlagValueSet represents a group of flags. This is a very simple example on how to implement this interface:
type myFlagSet struct {
flags []myFlag
}
func (f myFlagSet) VisitAll(fn func(FlagValue)) {
for _, flag := range flags {
fn(flag)
}
}
Once your flag set implements this interface, you can simply tell Viper to bind it:
fSet := myFlagSet{
flags: []myFlag{myFlag{}, myFlag{}},
}
viper.BindFlagValues("my-flags", fSet)
Getting Values From Viper
In Viper, there are a few ways to get a value depending on the value's type.
The following functions and methods exist:
Get(key string) : interface{}
GetBool(key string) : bool
GetFloat64(key string) : float64
GetInt(key string) : int
GetString(key string) : string
GetStringMap(key string) : map[string]interface{}
GetStringMapString(key string) : map[string]string
GetStringSlice(key string) : []string
GetTime(key string) : time.Time
GetDuration(key string) : time.Duration
IsSet(key string) : bool
One important thing to recognize is that each Get function will return a zero
value if it’s not found. To check if a given key exists, the IsSet() method
has been provided.
Example:
viper.GetString("logfile") // case-insensitive Setting & Getting
if viper.GetBool("verbose") {
fmt.Println("verbose enabled")
}
Accessing nested keys
The accessor methods also accept formatted paths to deeply nested keys. For
example, if the following JSON file is loaded:
{
"host": {
"address": "localhost",
"port": 5799
},
"datastore": {
"metric": {
"host": "127.0.0.1",
"port": 3099
},
"warehouse": {
"host": "198.0.0.1",
"port": 2112
}
}
}
Viper can access a nested field by passing a . delimited path of keys:
GetString("datastore.metric.host") // (returns "127.0.0.1")
This obeys the precedence rules established above; the search for the path
will cascade through the remaining configuration registries until found.
For example, given this configuration file, both datastore.metric.host and
datastore.metric.port are already defined (and may be overridden). If in addition
datastore.metric.protocol was defined in the defaults, Viper would also find it.
However, if datastore.metric was overridden (by a flag, an environment variable,
the Set() method, …) with an immediate value, then all sub-keys of
datastore.metric become undefined, they are “shadowed” by the higher-priority
configuration level.
Lastly, if there exists a key that matches the delimited key path, its value
will be returned instead. E.g.
{
"datastore.metric.host": "0.0.0.0",
"host": {
"address": "localhost",
"port": 5799
},
"datastore": {
"metric": {
"host": "127.0.0.1",
"port": 3099
},
"warehouse": {
"host": "198.0.0.1",
"port": 2112
}
}
}
GetString("datastore.metric.host") // returns "0.0.0.0"
Extract sub-tree from Viper.
For example, viper represents:
app:
cache1:
max-items: 100
item-size: 64
cache2:
max-items: 200
item-size: 80
After executing:
subv := viper.Sub("app.cache1")
subv represents:
max-items: 100
item-size: 64
Suppose we have:
func NewCache(cfg *Viper) *Cache {...}
which creates a cache based on config information formatted as subv.
Now it's easy to create these 2 caches separately as:
cfg1 := viper.Sub("app.cache1")
cache1 := NewCache(cfg1)
cfg2 := viper.Sub("app.cache2")
cache2 := NewCache(cfg2)
Unmarshaling
You also have the option of Unmarshaling all or a specific value to a struct, map,
etc.
There are two methods to do this:
Unmarshal(rawVal interface{}) : error
UnmarshalKey(key string, rawVal interface{}) : error
Example:
type config struct {
Port int
Name string
PathMap string `mapstructure:"path_map"`
}
var C config
err := Unmarshal(&C)
if err != nil {
t.Fatalf("unable to decode into struct, %v", err)
}
Viper or Vipers?
Viper comes ready to use out of the box. There is no configuration or
initialization needed to begin using Viper. Since most applications will want
to use a single central repository for their configuration, the viper package
provides this. It is similar to a singleton.
In all of the examples above, they demonstrate using viper in it's singleton
style approach.
Working with multiple vipers
You can also create many different vipers for use in your application. Each will
have it’s own unique set of configurations and values. Each can read from a
different config file, key value store, etc. All of the functions that viper
package supports are mirrored as methods on a viper.
Example:
x := viper.New()
y := viper.New()
x.SetDefault("ContentDir", "content")
y.SetDefault("ContentDir", "foobar")
//...
When working with multiple vipers, it is up to the user to keep track of the
different vipers.
Q & A
Q: Why not INI files?
A: Ini files are pretty awful. There’s no standard format, and they are hard to
validate. Viper is designed to work with JSON, TOML or YAML files. If someone
really wants to add this feature, I’d be happy to merge it. It’s easy to specify
which formats your application will permit.
Q: Why is it called “Viper”?
A: Viper is designed to be a companion
to Cobra. While both can operate completely
independently, together they make a powerful pair to handle much of your
application foundation needs.
Q: Why is it called “Cobra”?
A: Is there a better name for a commander?