Personal website of Martin Tournoij (“arp242”); writing about programming (CV) and various other things.

Working on GoatCounter and moreGitHub Sponsors.

Contact at martin@arp242.net or GitHub.

This page's author

I like commandline flags for configuration. It’s simple, robust, flexible, and generally just bullshit-free. See flags are great for configuration.

The way I do it in Go is keeping a list of exported variables in a cfg package:

// Package cfg handles the application configuration.
package cfg

// Configuration variables.
var (
    Prod         bool   // Production mode; hide errors.
    Domain       string // Domain, including protocol.
    // [..]
)

// Set configuration variables from os.Args.
func Set() {
    flag.BoolVar(&Prod, "prod", false, "Production mode; hide errors.")
    flag.StringVar(&Domain, "domain", "http://localhost:8080", "Domain, including protocol.")
    // [..]
}

And then call cfg.Set() from your main() function.

That’s it. Somewhere north of 75% of all applications will never need anything more than this.

I often use a simple program to generate the cfg.go file as it avoids duplicating the flag and variable names a few times. See the end of this post.

If all of this seems really trivial to you then you’re correct. Yet I encounter plenty of fairly simple projects (Go and others) in the wild that have complex configuration schemes just to load a few settings.

Using flags for large and complex applications is not a good idea. Don’t try it for your Postfix competitor. But most applications are not complex. The project I worked on this week has 2.8k lines of code and 8 settings. You don’t need 2.2k lines of code from Viper (and 9.5k from YAML lib) for that. It doesn’t really make things easier for anyone.


Some people might object to the use of package globals. I think it’s fine. It’s a tiny package (no room for confusion), they’re set only once (no race conditions), there is never any reason to create more than one instance, and it should be easy enough to set up for tests. In practice, only a few values tend to be referenced outside of the main() function (usually cfg.Prod and cfg.Domain).

I certainly consider this vastly superior to Viper’s untyped viper.GetBool("prod").

As mentioned in my other article, passing values from the environment can be done from the commandline prog -domain "$DOMAIN").

The stdlib flag package is enough for most programs. Some other libraries (e.g. cobra) make some operations a bit easier if you have a lot of subcommands like git. The basic idea remains the same.

P.S. If you’re going to use a configuration package then I recommend sconfig, because that’s what I wrote and it’s perfect ;-)

Generate script

I usually save it as cfg/gen.go.

Note: use go run cfg/gen.go > cfg/cfg.go the first time since the //go:generate is in the generated file. After that go generate ./cfg will work.

// +build go_run_only

package main

import (
    "bytes"
    "fmt"
    "go/format"
    "os"
    "strings"
    "text/template"
)

// Add your flags here.
var flags = []flag{
    flag{"bool", "Prod", "prod", false, "Production mode; hide errors."},
    flag{"string", "Domain", "domain", "http://localhost:8080", "Domain, including protocol."},
}

type flag struct {
    Type    string      // Go type name (e.g. bool, string, etc.)
    Name    string      // Variable name in the cfg package (e.g. Listen).
    Flag    string      // Flag name (e.g. "listen")
    Default interface{} // Default value.
    Help    string      // Help text.
}

func main() {
    longest := 0
    for _, f := range flags {
        if len(f.Name) > longest {
            longest = len(f.Name)
        }
    }

    var buf bytes.Buffer
    err := tpl.Execute(&buf, struct {
        Flags   []flag
        Longest int
    }{flags, longest})
    if err != nil {
        _, _ = fmt.Fprintf(os.Stderr, "template: %s\n", err)
        os.Exit(1)
    }

    src, err := format.Source(buf.Bytes())
    if err != nil {
        _, _ = fmt.Fprintf(os.Stderr, "gofmt: %s\n", err)
        os.Exit(1)
    }

    fmt.Print(string(src))
}

var tpl = template.Must(template.New("").
    Option("missingkey=error").
    Funcs(template.FuncMap{
        "ucfirst": func(s string) string { return strings.Title(s) },
        "pad":     func(s string, l int) string { return s + strings.Repeat(" ", l-len(s)) },
    }).
    Parse(`//go:generate sh -c "go run gen.go > cfg.go"

// Code generated by gen.go; DO NOT EDIT.

// Package cfg handles the application configuration.
package cfg

import (
    "flag"
    "fmt"
)

// Configuration variables.
var ({{range $f := .Flags}}
    {{$f.Name}} {{$f.Type}} // {{.Help}}{{end}}
)

// Set configuration variables from os.Args.
func Set() {{"{"}}{{range $f := .Flags}}
    flag.{{$f.Type|ucfirst}}Var(&{{$f.Name}}, "{{$f.Flag}}", {{printf "%#v" $f.Default}}, "{{$f.Help}}"){{end}}
    flag.Parse()
}

// Print out all configuration values.
func Print() {{"{"}}{{range $f := .Flags}}
    fmt.Printf("{{pad $f.Name $.Longest}}   %#v\n", {{$f.Name}}){{end}}
}
`))