Command-Line Flags
Define typed command-line options with the flag package, and know when a flag is a better choice than an environment variable.
addr := flag.String("addr", ":8080", "address to listen on"); flag.Parse()A flag is an option typed after the program name on the command line, like -addr=:9000. The standard library's flag package turns those words into typed Go values and writes the usage text for you.
Defining and parsing
Each flag is declared before parsing, in one of two forms that do exactly the same thing. flag.StringVar(&addr, "addr", ":8080", "address to listen on") fills a variable you already have. addr := flag.String("addr", ":8080", "address to listen on") is the shorthand: the package makes the variable itself and hands back a pointer to it, so you read it as *addr. There are matching pairs for Int, Bool and Duration.
Then call flag.Parse() once, at the top of main. Before that call, every flag variable still holds its default. Anything left over that is not a flag is available from flag.Args().
Flag or environment variable
Flags suit programs a person runs and varies run to run: command-line tools, migration runners, one-off jobs. They are self-documenting, since -h prints every flag. Environment variables suit long-running services started by a platform, and are the only sensible home for secrets, because a command line is visible to anyone who can list processes. A common arrangement is a flag whose default comes from an environment variable.
What trips people up
The package stops parsing at the first argument that is not a flag. mytool report.txt -verbose leaves -verbose unset and sitting in flag.Args(). Put flags first.
Example
package main
import (
"flag"
"fmt"
"os"
"time"
)
func main() {
// A FlagSet is a private set of flags. flag.String and friends use one
// hidden global set; making your own keeps this code testable.
fs := flag.NewFlagSet("serve", flag.ContinueOnError)
// Long form: the parsed value is written into a variable you declared.
var addr string
fs.StringVar(&addr, "addr", ":8080", "address to listen on")
// Short form: the same flag, but the FlagSet declares the variable and
// returns a pointer to it, so you read it as *timeout.
timeout := fs.Duration("timeout", 5*time.Second, "per-request timeout")
verbose := fs.Bool("verbose", false, "log every request")
// A real program passes os.Args[1:] here.
args := []string{"-addr=:9000", "-verbose", "report.txt"}
if err := fs.Parse(args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
fmt.Printf("addr=%s timeout=%s verbose=%t\n", addr, *timeout, *verbose)
fmt.Println("files:", fs.Args()) // whatever was not a flag
}
// Output:
// addr=:9000 timeout=5s verbose=true
// files: [report.txt]
When to use it
- Give a migration tool a -steps option and a -dry-run switch without inventing a config file.
- Let a developer override the listen address of a service that normally reads it from the environment.
- Build one binary with serve, migrate and seed subcommands, each with its own flags.
More examples
A flag whose default comes from the environment
This is the layering almost every service ends up with: built-in default, overridden by the environment, overridden by an explicit flag. Because the environment is read while the flag is being defined, -h prints the live default, which makes the help text honest about what the program would actually do.
// The platform sets APP_ADDR in production; a developer overrides it with
// -addr on the command line. One setting, two ways in, no duplication.
func envOr(key, fallback string) string {
if v, ok := os.LookupEnv(key); ok {
return v
}
return fallback
}
func main() {
addr := flag.String("addr", envOr("APP_ADDR", ":8080"), "address to listen on")
flag.Parse()
log.Println("listening on", *addr)
}A flag that can be given more than once
The built-in helpers each hold one value, so a second -tag would overwrite the first. flag.Func gives you the raw text per occurrence and lets you return an error, which the package turns into a usage message. Use it for repeatable options and for values that need validating as they are read, such as a URL or a file that must exist.
var tags []string
// flag.Func, added in Go 1.16, calls your function each time the flag
// appears, so -tag can be repeated: -tag db -tag slow
flag.Func("tag", "tag to attach; repeatable", func(s string) error {
if s == "" {
return errors.New("tag must not be empty")
}
tags = append(tags, s)
return nil
})
flag.Parse()
fmt.Println(tags) // [db slow]Subcommands, each with its own flags
The flag package has no notion of subcommands, but a FlagSet per verb is all that is needed: read the verb yourself from os.Args[1], then hand the rest to that verb's own set. ExitOnError makes the set print usage and exit on a bad flag, which is what a command-line tool should do; ContinueOnError returns the error instead, which is what a test wants.
// A tool with several verbs: mytool serve -addr=:9000, mytool migrate -steps=2
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: mytool serve|migrate [flags]")
os.Exit(2)
}
switch os.Args[1] {
case "serve":
fs := flag.NewFlagSet("serve", flag.ExitOnError)
addr := fs.String("addr", ":8080", "address to listen on")
fs.Parse(os.Args[2:]) // ExitOnError means this never returns on a bad flag
serve(*addr)
case "migrate":
fs := flag.NewFlagSet("migrate", flag.ExitOnError)
steps := fs.Int("steps", 1, "how many migrations to apply")
fs.Parse(os.Args[2:])
migrate(*steps)
default:
fmt.Fprintln(os.Stderr, "unknown command", os.Args[1])
os.Exit(2)
}
}
Discussion