How do you parse CLI flags with the flag package? How do subcommands work, and why is flag.Parse in init() a bug?
You define flags (flag.Int, flag.StringVar, flag.Func, flag.TextVar, flag.BoolFunc), call flag.Parse() once in main, and read flag.Args() for the positional arguments. For subcommands, create a separate flag.NewFlagSet for each one and parse os.Args[2:]:
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: app <serve|migrate> [flags]")
os.Exit(2)
}
switch os.Args[1] {
case "serve":
fs := flag.NewFlagSet("serve", flag.ExitOnError)
port := fs.Int("port", 8080, "listen port")
debug := fs.Bool("debug", false, "verbose logging")
fs.Parse(os.Args[2:])
fmt.Println("serve", *port, *debug, fs.Args())
case "migrate":
fs := flag.NewFlagSet("migrate", flag.ExitOnError)
steps := fs.Int("steps", 0, "number of steps (0 = all)")
fs.Parse(os.Args[2:])
fmt.Println("migrate", *steps)
default:
fmt.Fprintf(os.Stderr, "unknown command %q\n", os.Args[1])
os.Exit(2)
}
}
Why flag.Parse() in init() is a bug: other packages, including testing, register their flags during their own initialization. Since Go 1.13, test flags are registered by testing.Init(), which the generated test main calls after all package inits have run. Parsing early therefore fails with flag provided but not defined: -test.v, or it misses flags defined later. It also makes the package impossible to import cleanly.
Gotchas: parsing stops at the first non-flag argument, so in app serve file.txt -port 9 the -port is not parsed. Bool flags need -debug=false, because -debug false sets debug to true and leaves "false" as a positional argument. Use ContinueOnError in libraries and tests.
More on More Standard Library Essentials
- Q542Explain io/fs and fs.FS. How do you walk a directory with filepath.WalkDir and test file-system code with testing/fstest.MapFS?
- Q543What is os.Root (Go 1.24), and how does it prevent path-traversal attacks compared to filepath.Join plus filepath.Clean?
- Q545Compare strconv with fmt for number/string conversion. What do strconv.ParseInt's base and bitSize arguments do, and how do you check for range errors?
- Q546When would you use encoding/binary, encoding/gob or protobuf for serialization? What are the byte-order and compatibility pitfalls?
- Q547How do signals work in Go (os/signal)? Why must the channel passed to signal.Notify be buffered?