Default flag values in Go

In contrast to many languages, Go has a very nice built-in package for parsing arguments passed to an application called flag. There are many libraries which build on top of it, e.g. kingpin, but as long as your application’s requirements for arguments are simple enough the standard library package is completely satisfactory.

The API is pretty simple: you define your flags by calling functions flag.Int, flag.String or flag.Duration which take three arguments, the name of the flag, default value and a usage guide, and return a pointer to a variable which will be populated by the actual value passed by the user. Alternatively, you can use the *Var functions which take an extra argument, a pointer to the variable, and don’t return anything.

Most people get bit at first by the need to call flag.Parse() to get the actual flag values. Because the flags are defined in the program code, not in some pre-step or an annotation, it can’t know when all flags are defined so it can’t parse then automatically, an explicit call is required.

A newcomer does something like this and wonders why it doesn’t work:

package main

import (
	"fmt"
	"flag"
)

func main() {
	var port int

	address := flag.String("address", "localhost", "Address to run at")
	flag.IntVar(&port, "port", 666, "Port to run at")

	fmt.Println(*address, port)
}

// go run main.go --address=my.domain --port=8798
//> localhost 666

Playground

Then goes off to StackOverflow or the documentation, adds a call to flag.Parse() and is happy:

package main

import (
	"fmt"
	"flag"
)

func main() {
	var port int

	address := flag.String("address", "localhost", "Address to run at")
	flag.IntVar(&port, "port", 666, "Port to run at")

	flag.Parse()

	fmt.Println(*address, port)
}

// go run main.go --address=my.domain --port=8798
//> my.domain 8798

Playground

Then the newcomer writes code like this quite routinely for months and everything is alright. Flags work nicely, default values are used when the flag is not provided, the passed values otherwise. As a program grows, the number of flags grows as well, the flag related code gets moved from main() to a separate function which nicely lists all flags and ends in flag.Parse().

Few iterations over, it becomes obvious that passing around bunch of variables is bothersome and it would be nice to group them into some config struct and pass that around:

package main

import (
	"fmt"
	"flag"
)

type config struct {
	address string
	port int
}

func main() {
	config := parseFlags()

	fmt.Println(config.address, config.port)
}

func parseFlags() config {
	var config config

	flag.StringVar(&config.address, "address", "localhost", "Address to run at")
	flag.IntVar(&config.port, "port", 666, "Port to run at")

	flag.Parse()
	
	return config
}

// go run main.go --address=my.domain --port=8798
//> my.domain 8798

Playground

Maybe, some time later, this object becomes more complex and it is convenient to write a constructor for it which takes care of initializing other fields as well:

package main

import (
	"fmt"
	"flag"
	"time"
)

type config struct {
	address string
	port int

	initializationTime time.Time
}

func newConfig(address string, port int) config {
	return config{
		address: address,
		port: port,
		initializationTime: time.Now(),
	}
}

func main() {
	config := parseFlags()

	fmt.Println(config.address, config.port)
}

func parseFlags() config {
	address := flag.String("address", "localhost", "Address to run at")
	port := flag.Int("port", 666, "Port to run at")

	config := newConfig(*address, *port)

	flag.Parse()
	
	return config
}

// go run main.go --address=my.domain --port=8798
//> localhost 666

Playground

WTF? What just happened? Why are the flags not being parsed anymore? They are parsed exactly when they are supposed to be parsed, after the constructor was called. I hope, it is obvious that the constructor needs to be called after flag.Parse().

As obvious as it looks in this step by step progression, it wasn’t so obvious last week when we merged similar code. No one realized what was happening, not the developer writing it, not the developer reviewing. Because it happened in an application which doesn’t take part in an integration test where the actual binary would be executed, the tests didn’t catch it either. Moreover, the default values were too close to the desired ones making it a bit harder to notice them when looking through the logs.

So this time, the lesson is not read the documentation, it is don’t forget the documentation.

We're looking for developers to help us save energy

If you're interested in what we do and you would like to help us save energy, drop us a line at jobs@enectiva.cz.

comments powered by Disqus