Abhinav Rai

Tale of 2 Go Snakes

Lets walk through the journey of 2 venomous and powerful snakes residing in dense Go-Forests. Namely: Viper and Cobra

Why do we need to use them? Personally you can do the tasks which they do by writing your own code and you don’t require them. But why to repeat when its already done. If its doing your use case without any overhead, feel free to use it.

Viper

Viper Viper

Viper is a tool which manages all the configurations your app requires. Be it reading from some json/yml file or from the environment, or from command line, it does it all.

Important things to know about Viper:

  • Viper uses the following precedence order. Each item takes precedence over the item below it:
  1. explicit call to Set-5
  2. flag-4
  3. env-3
  4. config-2
  5. key/value store-1
  6. default-0

default: viper.SetDefault("DATABASE_HOST", "localhost"). 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.

Reading Config files: There are 2 ways.

  1. Use a yaml/json file and read with ReadInConfig() by setting the filename and path
  2. Use a bytes buffer and read with ReadConfig() by setting the filetype and passing the bytes buffer to it.

Setting Overrides: viper.Set("DATABASE_HOST", "localhost") Manually setting the config key. Its the highest priority and Get call will return this value if set is there.

Environment Variables:

v = viper.New()
  1. If you call v.AutomaticEnv() — 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. Eg. EnvPrefix = viablimp, key = Database_Driver, it will search for VIABLIMP_DATABASE_DRIVER as environment variable
v.BindEnv("id", "viablimp")
v.SetEnvPrefix("prefix")
os.Setenv("viablimp", "value")
id := v.Get("id") // "value"
v.BindEnv("database")
v.SetEnvPrefix("application")
os.Setenv("APPLICATION_DATABASE", "value")
database := v.Get("database")

The bind environment binds the environment with a key. I rarely use this.

Instead I use the below approach. I have a yml file called application.yml and I use these configurations for local.

Database:
  Host: "localhost"
  Port: 5432
  Username: "viablimp"
  Password: "password"
Log:
  Level: "info"

And in my production environment, I use environment variable.

v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

Nested viper objects can be retrieved by ‘.’ convention. Eg Database.Name Whereas the the env variables that we set are of the form Database_Name.

SetEnvKeyReplacer helps to convert . to _

So in our environment variable, we can have DATABASE_HOST and in ReadInConfig , environment variable will be picked.

func LoadConfig(filename string) (*Config, error) {
	v := viper.New()
	/**
	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.
	Eg. EnvPrefix = viablimp, key = Database_Driver, it will search for VIABLIMP_DATABASE_DRIVER as environment variable
	*/
	v.AutomaticEnv()

	/**
	Nested viper objects can be retrieved by '.' convention. Eg Database.Name
	Whereas the the env variables that we set are of the form Database_Name.
	SetEnvKeyReplacer helps to convert . to _
	*/
	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	v.SetConfigType("yaml")
	v.SetConfigName(filename)

	// Get a default config path
	// DEFAULT_CONFIG_PATH is set in environment variables
	defaultPath := v.GetString("DEFAULT_CONFIG_PATH")
	if defaultPath != "" {
		v.AddConfigPath(defaultPath)
	}

	/**
	This defaults to the base Current working directory and which in this case is the theflyingmantis/viablimp
	This has no relation to where the object file for go is being called
	*/
	v.AddConfigPath(".")
	fmt.Print(filepath.Abs("."))

	if err := v.ReadInConfig(); err != nil {
		//logger.Errorf("could not read the config %s", err.Error())
		return nil, err
	}
	loadedConfig := &Config{}
	if err := v.Unmarshal(loadedConfig); err != nil {
		//logger.Errorf("could not unmarshal the config %s", err.Error())
		return nil, errors.New("could not decode")
	}
	//logger.Info("loading config successfully")
	return loadedConfig, nil

}

Where Config is the struct you want to load into.

Cobra

Cobra Cobra

I use Cobra to make command line applications.

Cobra is built on a structure of commands, arguments & flags. Commands represent actions, Args are things and Flags are modifiers for those actions.

The best applications will read like sentences when used. Users will know how to use the application because they will natively understand how to use it.

The pattern to follow is APPNAME VERB NOUN --ADJECTIVE. or APPNAME COMMAND ARG --FLAG

A few good real world examples may better illustrate this point.

In the following example, ‘server’ is a command, and ‘port’ is a flag:

hugo server --port=1313

In this command we are telling Git to clone the url bare.

git clone URL --bare

General pattern is that there is one root command and then we keep on adding commands to that root command and repeat the cycle recursively. for every command, we execute some task with it. To get attributes for the task we use flags. If we want to set configurations from these flags, we use BindPFlags() else we Flags().

Great read can be found here and on the official documentation.

You may contact the author at me@abhinavrai.com. Also, its okay to clap for the 2 snakes!