skip to content

Flags and Subcommands

The flag package binds -name=value to typed variables and stops at the first non-flag argument, and flag.NewFlagSet is how you build subcommands without reaching for a dependency.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

In Go's flag package, why does flag.String return a *string, and when may you read it?

level: juniorimportance: must knowfreq 70%

answer

  1. the flag exists before argv is read
  2. you get a handle, not a value
  3. one call fills every registered flag
  4. dereference only after that call returns

basics

~20 s

flag.String registers a flag before the command line has been read, so it can only hand back a pointer to storage it will fill in later. Call flag.Parse() first, then read the value as *ptr.

solid answer

~40 s

Package `flag` works in three ordered steps: define, parse, read. `flag.String("name", "world", "who to greet")` runs at a point where nothing has looked at the command line yet, so all it can do is allocate a string, store the default in it, and return a `*string` it promises to overwrite. `flag.Parse()` reads `os.Args[1:]` and assigns into every registered flag; only after it returns is `*name` meaningful. The `Var` forms — `flag.StringVar(&cfg.Name, "name", "world", ...)` — bind to a variable you already own instead of allocating, which is how you fill a config struct directly. Whatever is left over after parsing is available as `flag.Args()`, with `flag.NArg()` for its length and `flag.Arg(0)` for the first operand. Two ordering mistakes cover most bugs: reading a flag before Parse, and defining one after Parse has already run.

code

go · 8 lines
go
name := flag.String("name", "world", "who to greet")
var quiet bool
flag.BoolVar(&quiet, "quiet", false, "suppress output")

flag.Parse() // reads os.Args[1:]

// Only meaningful from here on.
fmt.Println(*name, quiet, flag.NArg(), flag.Args())

go deeper

for a junior

Be ready to write the three lines from memory: define the flag, call flag.Parse, then dereference. Say out loud that reading the pointer before Parse gives you the default.

for a middle

Explain why the pointer exists at all — the flag is declared before the command line has been read — and show the Var forms filling a config struct instead.

for a senior

Show that you know the failure is silent: an unparsed or late-defined flag raises no error, it just serves the default. Mention flag.Value and flag.Var for the types the package does not cover.

for a principal

Own the shape of what you register: which values are flags, which have defaults safe enough to ship, and whether the parsed values land in one config struct that the rest of the program can be tested against.

## The three-step contract Go's standard library parses command lines with package `flag`, and the whole package is built around one ordering rule: **define every flag, then parse once, then read**. ### 1. Define ```go name := flag.String("name", "world", "who to greet") ``` This call happens long before anybody has looked at the command line. There is nothing for it to return except a *handle*: it allocates a `string`, writes the default `"world"` into it, records the flag under the name `name` on the default flag set, and returns the address. The same shape exists for the other supported types — `flag.Int`, `flag.Int64`, `flag.Uint`, `flag.Float64`, `flag.Bool`, `flag.Duration` (which reads values like `1500ms` or `2h` into a `time.Duration`) — each returning a pointer to its own type. Every one of them has a `Var` twin that takes the address of a variable you already have: ```go var cfg struct { Name string Quiet bool } flag.StringVar(&cfg.Name, "name", "world", "who to greet") flag.BoolVar(&cfg.Quiet, "quiet", false, "suppress output") ``` The `Var` forms are what you use when the parsed values should land in a config struct rather than in a scatter of package-level pointers. They are otherwise identical: same names, same defaults, same usage strings. ### 2. Parse `flag.Parse()` reads `os.Args[1:]` and walks it, assigning into the registered flags. Until it is called, every pointer you were handed still holds its default. That is the single most common beginner bug in this package, and it is silent: nothing panics, nothing logs, the program simply behaves as if the user passed nothing. The mirror-image bug is defining a flag *after* `Parse` has returned. Registration succeeds, but the parse that would have filled it has already happened, so the flag holds its default forever. Define everything first — usually in `main` before any other work, or in package-level `var` declarations. ### 3. Read After `Parse` returns, `*name` is the value the user asked for, or the default if they said nothing. There is deliberately no way to tell those two apart from the value alone — `flag.Visit` exists for that. ## What is left over Flags are only part of a command line; the rest are *operands*. After parsing: - `flag.Args()` returns the remaining arguments as a `[]string` - `flag.NArg()` is `len(flag.Args())` - `flag.Arg(i)` indexes it safely, returning `""` when `i` is out of range None of these include the program name — that is `os.Args[0]`, and it never reaches the parser. ## Extending it: the Value interface The fixed set of types runs out quickly. A flag of any type at all is possible through `flag.Var`, which accepts anything implementing: ```go type Value interface { String() string Set(string) error } ``` `Set` is called once for **each occurrence** of the flag on the command line, which is exactly how you build a repeatable flag: append in `Set` and you get a slice. `String` is called to render the value in usage text, so it must tolerate being called on a zero value of the type. `flag.Func` is a shortcut for the same idea when you only need the `Set` half and no accumulated state. ## Why the pointer, once more Some people find the returned pointer awkward and reach for `os.Args` directly. That trade is a bad one: hand-parsing loses the generated `-h` usage text, the type conversion, the default values, and the consistent syntax other Go tools share. The pointer is simply the price of the flag being declared before the data exists — and if you dislike it, the `Var` forms remove it entirely.

  • What is the difference between flag.String and flag.StringVar?
    `flag.String` allocates a string, seeds it with the default, and returns a `*string`. `flag.StringVar` takes the address of a variable you already own and writes into that instead. Everything else — name, default, usage text, parsing — is identical. Use the `Var` form when the values belong in a config struct rather than in loose package-level pointers.
  • How would you accept a flag the user can repeat, such as -exclude twice?
    Define a named slice type with `String() string` and `Set(string) error` methods, so it satisfies `flag.Value`, and register it with `flag.Var`. `Set` is invoked once per occurrence of the flag, so appending inside `Set` accumulates every value. `flag.Func` does the same job when you need only the `Set` behaviour and no stored state.
  • What does flag.Args() give you, and how does it differ from os.Args?
    `os.Args` is the raw argument vector, with the program path at index 0 and nothing interpreted. `flag.Args()` is what the parser did not consume — the positional operands left after flag parsing stopped, with no program name. `flag.NArg()` is its length and `flag.Arg(i)` indexes it, returning an empty string when `i` is out of range.

Registering a flag is like handing someone an empty envelope with your address on it: you get the envelope immediately, but there is nothing inside until the mail is actually sorted.

saying these in an interview costs you the question

  • Reads the flag pointer before calling flag.Parse
  • Defines flags after flag.Parse has already run
  • Thinks flag.Args() includes the program name
  • Believes flag.String returns the value rather than a pointer
  • Hand-parses os.Args instead of registering flags
open as a page

How do you implement git-style subcommands in Go with flag.NewFlagSet, each owning its own flags?

level: middleimportance: must knowfreq 55%

basics

~10 s

Give each subcommand its own *flag.FlagSet from flag.NewFlagSet, switch on os.Args[1] to choose one, and call that set's Parse on os.Args[2:]. Each set owns its own flag names, usage text and operands.

open as a page

Why does Go's flag package ignore -v in `tool build main.go -v`, and what does `--` do?

level: middleimportance: should knowfreq 50%

basics

~20 s

Go's flag package stops parsing at the first argument that is not a flag, so -v after main.go is treated as a positional operand and never assigned. A bare -- also ends parsing, and everything after it becomes an operand.

open as a page

In a Go CLI with per-subcommand flag sets, why is a -verbose flag silently false, and how do you catch it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Because nothing parsed the set it was registered on. A flag defined on one flag set and never reached by that set's Parse keeps its default, with no error anywhere, and the default is indistinguishable from a deliberate choice.

open as a page

You own a Go CLI other teams call from CI. How do you decide what becomes a subcommand, a flag, or a positional?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Treat the command line as a public API. Subcommands for distinct verbs, flags for anything optional or likely to change, positionals only for the one obvious operand — Go's flag package offers no alias or deprecation mechanism once a name ships.

open as a page