8.2 flag — Junior¶
Audience. You can write a Go program with
os.Args[1:]but the result feels brittle. Boolean toggles parse wrong, the help message doesn't exist, and any time you add a fourth argument the parsing code grows anotherifbranch. By the end of this file you will know the eight functions that cover 90% offlaguse, the rule that turns half the bugs into compile errors, and the shape of a small but real CLI program.
1. Why not just os.Args[1:]?¶
os.Args is a []string. The first element is the program name; the rest are whatever the shell handed you, in order. Parsing them by hand gets ugly fast.
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
if len(os.Args) < 3 {
fmt.Fprintln(os.Stderr, "usage: prog <count> <name>")
os.Exit(2)
}
count, err := strconv.Atoi(os.Args[1])
if err != nil {
fmt.Fprintln(os.Stderr, "count must be an integer")
os.Exit(2)
}
name := os.Args[2]
for i := 0; i < count; i++ {
fmt.Println("hello,", name)
}
}
This is fine for a two-argument throwaway. As soon as you want optional flags (--verbose), default values, or a -h that prints documentation, hand-rolled parsing turns into a small interpreter. The standard library already has one.
2. The four-line program¶
package main
import (
"flag"
"fmt"
)
var name = flag.String("name", "world", "who to greet")
func main() {
flag.Parse()
fmt.Println("hello,", *name)
}
What just happened:
flag.String("name", "world", "who to greet")registered a flag calledname, with default value"world"and a one-line description used in the help output. It returned a*stringthat points at a package-level variable theflagpackage owns.flag.Parse()walkedos.Args[1:], matched the-name=...or-name ...arguments against the registered flags, and assigned to the variable.*namedereferences the pointer to get the parsed value.
Run it:
$ ./greet
hello, world
$ ./greet -name=Bakhodir
hello, Bakhodir
$ ./greet -name Bakhodir
hello, Bakhodir
$ ./greet -h
Usage of ./greet:
-name string
who to greet (default "world")
The -h and --help flags are handled automatically: flag prints a generated usage block to os.Stderr and exits with status 2.
3. The cardinal rule: flag.Parse runs first¶
Every flag-defining call (flag.String, flag.Int, flag.Var, etc.) returns a pointer immediately, but the pointer holds the default value until flag.Parse runs. Reading the variable before Parse gives you the default, not the user input.
var port = flag.Int("port", 8080, "listen port")
func main() {
log.Println("starting on", *port) // BUG: prints 8080 always
flag.Parse()
}
The fix is a one-line move: every read of a flag variable must come after flag.Parse(). As a habit, put flag.Parse() as the very first statement in main.
The reverse mistake — defining a flag after Parse — is also broken. The new flag is registered but never gets a chance to be parsed; it keeps its default forever. Define all flags at package scope or at the top of main, then call Parse.
4. The pointer style and the variable style¶
flag.String("name", "world", "...") gives you a *string. There is also a flag.StringVar form that writes into a variable you already have:
var name string
func init() {
flag.StringVar(&name, "name", "world", "who to greet")
}
func main() {
flag.Parse()
fmt.Println("hello,", name) // no dereference
}
StringVar (and IntVar, BoolVar, DurationVar, Float64Var, Int64Var, Uint64Var, Uint64Var) is mostly a style choice. Use it when:
- The variable wants to live in a struct field.
- You want to spell out the type rather than read it from the call.
- Multiple flags should write into the same variable (rare, usually a smell, but legal).
Otherwise the pointer-returning form is more compact.
5. The built-in types¶
The flag package ships with one defining function per common type. Each comes in a pointer-returning form and a *Var form.
| Type | Pointer form | Var form |
|---|---|---|
string | flag.String(name, def, usage) | flag.StringVar(&v, ...) |
int | flag.Int(name, def, usage) | flag.IntVar(&v, ...) |
int64 | flag.Int64(...) | flag.Int64Var(...) |
uint | flag.Uint(...) | flag.UintVar(...) |
uint64 | flag.Uint64(...) | flag.Uint64Var(...) |
bool | flag.Bool(...) | flag.BoolVar(...) |
float64 | flag.Float64(...) | flag.Float64Var(...) |
time.Duration | flag.Duration(...) | flag.DurationVar(...) |
| anything else | flag.Var(value, name, usage) | (same) |
flag.Var takes a value that satisfies the flag.Value interface, which is two methods (String() and Set(string) error). Custom flag types live there — covered in middle.md. For now, everything you need is in the table above.
6. Numbers and durations¶
var (
port = flag.Int("port", 8080, "listen port")
timeout = flag.Duration("timeout", 30*time.Second, "request timeout")
rate = flag.Float64("rate", 1.0, "messages per second")
)
The parse rules come from strconv and time.ParseDuration:
- Integers accept decimal (
8080), hex (0x1f90), octal (0o17620), and binary (0b1111100110000). Negative numbers work for signed types only. - Floats accept the usual
1.5,1e6, and0x1p-2(hex float). - Durations are spelled with units:
300ms,1.5h,45s,2h45m. Bare integers are not durations —flag.Duration("timeout", ...)with-timeout=30is a parse error.
$ ./prog -port=80 -timeout=2s -rate=10
$ ./prog -port=foo
invalid value "foo" for flag -port: parse error
Usage of ./prog: ...
exit status 2
A bad value goes to os.Stderr along with the usage, and the program exits with status 2.
7. Boolean flags and their one weird quirk¶
var verbose = flag.Bool("verbose", false, "enable verbose logging")
func main() {
flag.Parse()
if *verbose {
log.SetFlags(log.LstdFlags | log.Lshortfile)
}
}
A boolean flag is set just by mentioning it: -verbose is the same as -verbose=true. To turn one off explicitly (only meaningful if the default is true), you must use -verbose=false. The form -verbose false (with a space) does not work for booleans:
This is the single sharpest edge in the package. The parser sees -verbose and, because it knows the flag is a bool, decides "no explicit value, treat as true." It then takes false as a positional argument. Always use = for boolean flags when you need to override them: -verbose=false.
For all non-boolean flags the space form -name value works fine; it only fails for booleans.
8. Reading non-flag arguments¶
After flag.Parse(), the leftover arguments are available through helpers:
flag.Parse()
fmt.Println("flags parsed:", flag.NFlag())
fmt.Println("positional:", flag.NArg(), flag.Args())
fmt.Println("first positional:", flag.Arg(0)) // "" if missing
flag.Args() returns a []string of arguments that were not flags. flag.NArg() is its length. flag.Arg(i) is bounds-checked — out-of- range returns the empty string.
A typical pattern: optional flags, then one or more file arguments.
var verbose = flag.Bool("v", false, "verbose")
func main() {
flag.Parse()
if flag.NArg() == 0 {
fmt.Fprintln(os.Stderr, "usage: prog [-v] file ...")
os.Exit(2)
}
for _, path := range flag.Args() {
process(path)
}
}
flag stops collecting flags at the first non-flag argument. Anything after that — even something that looks like a flag — is passed through as a positional argument. This is how prog -v -- -not-a-flag keeps -not-a-flag out of the parser: the bare -- marks the end of flags.
9. Single dash, double dash¶
flag accepts both -name and --name. The two are exactly equivalent:
This differs from GNU getopt conventions, where -n is short and --name is long. The standard flag package has no notion of short flags. If you want a short alias, you register two flags that share a variable:
var verbose bool
func init() {
flag.BoolVar(&verbose, "verbose", false, "enable verbose logging")
flag.BoolVar(&verbose, "v", false, "shorthand for -verbose")
}
Now both -v and -verbose set the same variable. The help output lists them as separate entries; that's the price.
10. The auto-generated usage message¶
flag prints a usage block when:
- The user passes
-hor--help(or any unknown flag). - You call
flag.Usage()yourself. - The parser hits a malformed flag and the default
ErrorHandlingmode (ExitOnError) kicks in.
The default usage prints Usage of <program>: followed by every registered flag with its type, default, and description. That is often enough for small tools.
When you want a richer usage — a one-line synopsis at the top, an "EXAMPLES" section, etc. — replace flag.Usage:
func init() {
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "usage: %s [flags] file ...\n", os.Args[0])
fmt.Fprintln(flag.CommandLine.Output(), "Process files with optional flags.")
fmt.Fprintln(flag.CommandLine.Output())
flag.PrintDefaults() // the auto-generated table
}
}
flag.PrintDefaults is the function that produces the per-flag table, and you can call it from inside your custom Usage to keep the table without rewriting it.
11. Where the usage goes¶
By default, usage and error messages go to os.Stderr. You can change the destination:
Or, for a custom FlagSet (see middle.md):
fs := flag.NewFlagSet("sub", flag.ExitOnError)
fs.SetOutput(io.Discard) // suppress all output during testing
In tests you usually want to capture usage in a buffer instead of spraying it to the test log. flag.CommandLine.SetOutput(&buf) does that.
12. Exit codes from flag¶
The default behavior on parse errors is flag.ExitOnError, which:
- Prints the error to the configured output (default
os.Stderr). - Prints the usage block.
- Calls
os.Exit(2).
The exit code is fixed at 2, matching the Unix convention for "command line usage error." If you want different behavior — to handle the error yourself, return it from main, or panic — you create your own *FlagSet with a different ErrorHandling mode. Covered in middle.md and senior.md.
For now, accept that flag.Parse() either succeeds or terminates the process. There is no third outcome with the global flag set.
13. Putting it together: a small head clone¶
package main
import (
"bufio"
"flag"
"fmt"
"io"
"os"
)
var (
n = flag.Int("n", 10, "number of lines to print")
quiet = flag.Bool("q", false, "suppress filename headers")
)
func head(in io.Reader, out io.Writer, lines int) error {
s := bufio.NewScanner(in)
for i := 0; i < lines && s.Scan(); i++ {
if _, err := fmt.Fprintln(out, s.Text()); err != nil {
return err
}
}
return s.Err()
}
func main() {
flag.Parse()
paths := flag.Args()
if len(paths) == 0 {
if err := head(os.Stdin, os.Stdout, *n); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
return
}
for i, p := range paths {
f, err := os.Open(p)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if !*quiet && len(paths) > 1 {
if i > 0 {
fmt.Println()
}
fmt.Printf("==> %s <==\n", p)
}
if err := head(f, os.Stdout, *n); err != nil {
f.Close()
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
f.Close()
}
}
Two flags, one positional list of files, sane defaults, automatic help. The head function takes io.Reader / io.Writer so it's testable without touching the filesystem — same shape as the I/O examples in 01-io-and-file-handling/junior.md.
14. Defining a flag twice — what happens¶
If two calls register the same name on the same FlagSet, the second call panics:
flag.String("port", "8080", "...")
flag.String("port", "9090", "...") // panic: flag redefined: port
The panic message is flag redefined: <name>. This catches you when two packages independently register the same global flag (e.g., a library you imported registers -config, and so do you). The standard library deliberately does not silently override; it forces you to rename.
If you need a flag whose presence is optional — set in some builds, absent in others — use a custom *FlagSet per package and merge manually. The shared global namespace is opt-in via flag.CommandLine.
15. The global flag.CommandLine¶
Everything you've called as flag.X is a thin wrapper over a package- level *FlagSet named flag.CommandLine. These two snippets are equivalent:
The global form is convenient. The explicit form is necessary the moment you want a second FlagSet for a subcommand, or you want to parse a slice that isn't os.Args[1:] (very common in tests). You will graduate to *FlagSet in middle.md; for now, know that flag.X is just sugar for flag.CommandLine.X.
16. Common errors at this level¶
| Symptom | Likely cause |
|---|---|
| Flag value is always the default | Reading the variable before flag.Parse() |
flag provided but not defined: -foo | Mistyped flag, or defined after Parse |
flag redefined: foo panic | Two registrations on the same FlagSet |
-verbose false doesn't disable | Boolean flag needs -verbose=false (with =) |
| Help message shows wrong default | Default literal in flag.X(name, def, ...) is the type's zero value, not what you assigned later |
flag.NArg() == 0 even though args were passed | Args looked like flags and were rejected; check for flag provided but not defined on stderr |
flag.Parse exits the program | Default ExitOnError mode; switch to ContinueOnError and handle yourself |
17. What flag does not do¶
It's worth knowing the wall before you walk into it:
- No required flags. Every flag has a default. If you need required behavior, check after
Parseand exit with a usage error yourself. - No short/long pairs.
-vand--verboseare independent registrations. - No abbreviation matching.
-veris not the same as-verboseeven if no other flag starts withver. - No environment-variable fallback. You wire it manually (covered in middle.md).
- No subcommands. You build them with one
*FlagSetper subcommand. - No completion generation. You write a separate command for that.
- No grouping in the help. All flags appear in one list, sorted by name.
- No
--short option of its own; bare--ends flag parsing, which is a feature, not a flag.
When you outgrow these, the usual graduation path is cobra (Kubernetes, Hugo, GitHub CLI) or urfave/cli (smaller, lighter). For most internal tools, you never need to graduate — flag plus thirty lines of glue is enough.