Let's Go Configuration and error handling › Managing configuration settings
Previous · Contents · Next
Chapter 3.1.

Managing configuration settings

Our web application’s main.go file currently contains a couple of hard-coded configuration settings:

Having these hard-coded isn’t ideal. There’s no separation between our configuration settings and code, and we can’t change the settings at runtime (which is important if you need different settings for development, testing and production environments).

In this chapter we’ll start to improve that, beginning by making the network address for our server configurable at runtime.

Command-line flags

In Go, a common and idiomatic way to manage configuration settings is to use command-line flags when starting an application. For example:

$ go run ./cmd/web -addr=":80"

The easiest way to accept and parse a command-line flag in your application is with a line of code like this:

addr := flag.String("addr", ":4000", "HTTP network address")

This essentially defines a new command-line flag with the name addr, a default value of ":4000" and some short help text explaining what the flag controls. The value of the flag will be stored in the addr variable at runtime.

Let’s use this in our application and swap out the hard-coded network address in favor of a command-line flag instead:

File: cmd/web/main.go
package main

import (
    "flag" // New import
    "log"
    "net/http"
)

func main() {
    // Define a new command-line flag with the name 'addr', a default value of ":4000"
    // and some short help text explaining what the flag controls. The value of the
    // flag will be stored in the addr variable at runtime.
    addr := flag.String("addr", ":4000", "HTTP network address")

    // Importantly, we use the flag.Parse() function to parse the command-line flag.
    // This reads in the command-line flag value and assigns it to the addr
    // variable. You need to call this *before* you use the addr variable
    // otherwise it will always contain the default value of ":4000". If any errors are
    // encountered during parsing the application will be terminated.
    flag.Parse()

    mux := http.NewServeMux()

    fileServer := http.FileServer(http.Dir("./ui/static/"))
    mux.Handle("GET /static/", http.StripPrefix("/static", fileServer))
    
    mux.HandleFunc("GET /{$}", home)
    mux.HandleFunc("GET /snippet/view/{id}", snippetView)
    mux.HandleFunc("GET /snippet/create", snippetCreate)
    mux.HandleFunc("POST /snippet/create", snippetCreatePost)

    // The value returned from the flag.String() function is a pointer to the flag
    // value, not the value itself. So in this code, that means the addr variable 
    // is actually a pointer, and we need to dereference it (i.e. prefix it with
    // the * symbol) before using it. Note that we're using the log.Printf() 
    // function to interpolate the address with the log message.
    log.Printf("starting server on %s", *addr)

    // And we pass the dereferenced addr pointer to http.ListenAndServe() too.
    err := http.ListenAndServe(*addr, mux)
    log.Fatal(err)
}

Save this file and try using the -addr flag when you start the application. You should find that the server now listens on whatever address you specify, like so:

$ go run ./cmd/web -addr=":9999"
2024/03/18 11:29:23 starting server on :9999

Default values

Command-line flags are completely optional. For instance, if you run the application with no -addr flag the server will fall back to listening on address ":4000" (which is the default value we specified).

$ go run ./cmd/web
2024/03/18 11:29:23 starting server on :4000

There are no rules about what to use as the default values for your command-line flags. I like to use defaults which make sense for my development environment, because it saves me time and typing when I’m building an application. But YMMV. You might prefer the safer approach of setting defaults for your production environment instead.

Type conversions

In the code above we’ve used the flag.String() function to define the command-line flag. This has the benefit of converting whatever value the user provides at runtime to a string type. If the value can’t be converted to a string then the application will print an error message and exit.

Go also has a range of other functions including flag.Int(), flag.Bool(), flag.Float64() and flag.Duration() for defining flags. These work in exactly the same way as flag.String(), except they automatically convert the command-line flag value to the appropriate type.

Automated help

Another great feature is that you can use the -help flag to list all the available command-line flags for an application and their accompanying help text. Give it a try:

$ go run ./cmd/web -help
Usage of /tmp/go-build3672328037/b001/exe/web:
  -addr string
        HTTP network address (default ":4000")

So, all in all, this is starting to look really good. We’ve introduced an idiomatic way of managing configuration settings for our application at runtime, and thanks to the -help flag, we’ve also got an explicit and documented interface between our application and its operating configuration.


Additional information

Environment variables

If you’ve built and deployed web applications before, then you’re probably thinking what about environment variables? Surely it’s good-practice to store configuration settings there?

If you want, you can store your configuration settings in environment variables and access them directly from your application by using the os.Getenv() function like so:

addr := os.Getenv("SNIPPETBOX_ADDR")

But this has some drawbacks compared to using command-line flags. You can’t specify a default setting (the return value from os.Getenv() is the empty string if the environment variable doesn’t exist), you don’t get the -help functionality that you do with command-line flags, and the return value from os.Getenv() is always a string — you don’t get automatic type conversions like you do with flag.Int(), flag.Bool() and the other command line flag functions.

Instead, you can get the best of both worlds by passing the environment variable as a command-line flag when starting the application. For example:

$ export SNIPPETBOX_ADDR=":9999"
$ go run ./cmd/web -addr=$SNIPPETBOX_ADDR
2024/03/18 11:29:23 starting server on :9999

Boolean flags

For flags defined with flag.Bool(), omitting a value when starting the application is the same as writing -flag=true. The following two commands are equivalent:

$ go run ./example -flag=true
$ go run ./example -flag

You must explicitly use -flag=false if you want to set a boolean flag value to false.

Pre-existing variables

It’s possible to parse command-line flag values into the memory addresses of pre-existing variables, using flag.StringVar(), flag.IntVar(), flag.BoolVar(), and similar functions for other types.

These functions are particularly useful if you want to store all your configuration settings in a single struct. As a rough example:

type config struct {
    addr      string
    staticDir string
}

...

var cfg config

flag.StringVar(&cfg.addr, "addr", ":4000", "HTTP network address")
flag.StringVar(&cfg.staticDir, "static-dir", "./ui/static", "Path to static assets")

flag.Parse()