Configuration in Go

December, 2019  |  

Gopher holding big flag and standing near retro computer

Introduction

Hi everyone! After working with Go for more than five years I have formed a strong opinion for a certain way of configuring programs. In this article I will cover the approach and share a small library, which is an implementation of these ideas.

It is worth mentioning, that the article is written based on my personal experience, so it is quite subjective and does not claim to be the ultimate truth. However, I hope it can be useful to the community and will help to reduce time spent on such a trivial task.

What is it all about?

In general, the configuration, in my opinion, is all about defining parameters and receiving their values from outside of our program during a runtime. It may be command-line parameters or arguments, environment variables, configuration files stored on the disk or somewhere on the network, a database table and so on.

Since Go’s type system is strong and static, we’d like to define and receive values for these parameters with knowledge of their type.

There are lots of already existing open source libraries or even frameworks solving such tasks. Most of them represent their own vision of how to do it.

But as stated above, I would like to cover, perhaps, less widely used approach to a program configuration. Especially since this approach seems much simpler.

The flag package

Yes, this is not a joke and I really would like to draw your attention to this well-known package of the Go’s standard library.

At the first glance, flag provides command-line arguments parsing and nothing more. But this package can also be used as an interface for program parameters definition. And in the context of the discussing approach, flag is used primarily in this way.

As mentioned above, we would like to have typed parameters. flag package supports the most basic types – flag.String(), flag.Int() and even flag.Duration(). For more complex types, such as []string or time.Time there is flag.Value interface which allows you to implement the parsing of the value from its string representation.

For example, the time.Time parameter can be implemented like this:

// TimeValue is an implementation of flag.Value interface.
type TimeValue struct {
	P      *time.Time
	Layout string
}

func (t *TimeValue) Set(s string) error {
	v, err := time.Parse(t.Layout, s)
	if err == nil {
		(*t.P) = v
	}
	return err
}

func (t *TimeValue) String() string {
	return t.P.Format(t.Layout)
}

An important property of the package is that it lives in the standard library. Thus, flag is the standard way of a program configuration. So, the probability of flag being reused between different projects or libraries is higher than for the other configuration libraries or frameworks.

Why is flag not being used?

In my opinion, other libraries exist and used for two main reasons:

The situation related to reading parameters is rather clear (for example, reading from files; nevertheless, we will talk about this later), yet it is necessary to say a few words about structured parameters here.

Often you can meet a particular way of defining the program parameters as a structure, i.e. where fields could be other structures and so on:

type AppConfig struct {
	Port int
	Database struct {
		Endpoint string
		Timeout  time.Duration
	}
	...
}

This is probably the main reason why there are configuration libraries and frameworks that allow you to work with parameters in such a way.

I think, flag should not provide the possibilities of a structured configuration. This can be achieved with a few lines of code (or with the flagutil library, which I will mention later).

Moreover, if you think about it, use of such a structure leads to hard coupling between all used components of the program.

Structured configuration

The idea is to define parameters independently and regardless of the program structure and as close as possible to the place where they are used – that is, directly on the package level.

Suppose we have an implementation of the client to some service (database, API or anything else), which is called yoogle:

package yoogle

type Config struct {
	Endpoint string
	Timeout  time.Duration
}

func New(c *Config) *Client {
	// ...
}

In order to fill the yoogle.Config we need a function that registers the fields of the structure within the received *flag.FlagSet.

This function can be defined in yoogle package or within yooglecfg package (in case of a third-party library, we could code such function somewhere else):

package yooglecfg

import (
	"flag"

	"app/yoogle"
)

func Export(flag *flag.FlagSet) *yoogle.Config {
	var c yoogle.Config
	flag.StringVar(&c.Endpoint,
		"endpoint", "https://example.com",
		"endpoint for our API",
	)
	flag.DurationVar(&c.Timeout,
		"timeout", time.Second,
		"timeout for operations",
	)
	return &c
}

In order to get rid of the flag package dependency we can define an interface with the required methods from flag.FlagSet:

package yooglecfg
	
import "app/yoogle"

type FlagSet interface {
	StringVar(p *string, name, value, desc string)
}

func Export(flag FlagSet) *yoogle.Config {
	var c yoogle.Config
	flag.StringVar(&c.Endpoint,
		"endpoint", "https://example.com",
		"endpoint for our API",
	)
	return &c
}

If configuration depends on certain values (for example, a parameter specifies an algorithm), the yooglecfg.Export() function could return a factory function, which must be called after parsing of all parameters values:

package yooglecfg
	
import "app/yoogle"

type FlagSet interface {
	StringVar(p *string, name, value, desc string)
}

func Export(flag FlagSet) func() *yoogle.Config {
	var algorithm string
	flag.StringVar(&algorithm,
		"algorithm", "quick",
		"algorithm used to do something",
	)

	var c yoogle.Config
	return func() *yoogle.Config {
		switch algorithm {
		case "quick":
			c.Impl = quick.New()
		case "merge":
			c.Impl = merge.New()
		case "bubble":
			panic(...)
		}
		return c
	}
}

Such export functions allow you to define package parameters regardless of their values parsing method or structure of the program configuration.

github.com/gobwas/flagutil

Now we’ve fixed that highly coupled configuration structure and made our parameters independent. But it is still not yet clear how to collect them all together and receive their values.

The flagutil package was written to solve exactly this problem.

Collecting parameters together

All parameters of program packages or third-party libraries receive their unique prefix and are collected at the main package:

package main

import (
	"flag"

	"app/yoogle"
	"app/yooglecfg"
 
	"github.com/gobwas/flagutil"
)

func main() {
	flags := flag.NewFlagSet("my-app", flag.ExitOnError)

	var port int
	flag.IntVar(&port, 
		"port", 4050,
		"port to bind to",
	)

	var config *yoogle.Config
	flagutil.Subset(flags, "yoogle", func(flags *flag.FlagSet) {
		config = yooglecfg.Export(flags)
	})
}

The flagutil.Subset() function does a simple thing: it adds a prefix ("yoogle") to all parameters defined within a callback.

In this case program execution may look like this:

app -port 4050 -yoogle.endpoint https://example.com -yoogle.timeout 10s

Getting parameter values

All parameters defined within flag.FlagSet contain an implementation of flag.Value, which has a Set(string) error method. That is, every parameter provides ability to set string representation of its value.

All we have to do now is to parse key-value pairs from any source and call flag.Set(key, value).

Note, with that you don’t even have to use the command-line syntax of the flag package. You can parse arguments in any way, for example, as posix program arguments.

package main

func main() {
	flags := flag.NewFlagSet("my-app", flag.ExitOnError)

	// ...

	flags.String(
		"config", "/etc/app/config.json", 
		"path to configuration file",
	)

	flagutil.Parse(flags,
		// First, use posix arguments syntax instead of `flag`.
		// Just to illustrate that it is possible.
		flagutil.WithParser(&pargs.Parser{
			Args: os.Args[1:],
		}),	

		// Then lookup for "config" flag value and try to
		// parse its value as a json configuration file.
		flagutil.WithParser(&file.Parser{
			PathFlag: "config",
			Syntax:   &json.Syntax{},
		}),
	)
}

Conclusion

Of course, I won’t be the first person who talks about such approach. Many of the ideas described above were already in use a few years ago, when I was working at MailRu.

So, in order to simplify the program configuration and not to spend time learning (or even writing) the next configuration framework the following is suggested:

The creation of the flagutil library was greatly inspired by the peterburgon/ff library – and I wouldn’t write flagutil if there hadn’t been certain design differences.

Thanks for your attention!

References