AlgoMaster Logo

flag Package

Medium Priority19 min readUpdated June 6, 2026

Almost every command-line tool needs to read options: a port number, a config path, a verbosity level, a date range. Go's standard library ships the flag package for exactly this job, and it's enough to build production CLIs without ever pulling in a third-party library. This chapter walks through how flag parses arguments, the two ways to declare flags, how to handle positional arguments, how to customize help output, how to wire up subcommands by hand, and how to plug in custom value types.

Declaring Flags

A flag is a named option a user passes on the command line. flag.String, flag.Int, flag.Bool, and friends each declare one, returning a pointer to a value that gets filled in when you call flag.Parse.

Run it like this:

Every flag declaration takes three things: the flag name (without dashes), a default value if the user doesn't pass it, and a usage string that shows up in -help output. The return value is a pointer because the actual value isn't known until Parse runs.

The full set of typed declarations the package ships:

FunctionReturnsDefault kind
flag.String(name, value, usage)*stringstring
flag.Int(name, value, usage)*intint
flag.Int64(name, value, usage)*int64int64
flag.Uint(name, value, usage)*uintuint
flag.Uint64(name, value, usage)*uint64uint64
flag.Float64(name, value, usage)*float64float64
flag.Bool(name, value, usage)*boolbool
flag.Duration(name, value, usage)*time.Durationduration string

flag.Duration is worth singling out. It accepts any string time.ParseDuration accepts, which means 500ms, 2s, 1h30m, and so on. This is how you'd typically take a timeout or a retry interval.

The strings are parsed for you, so an invalid duration like -timeout 5xs produces a parse error and the program exits with status 2.

Pointer-Returning vs Var Variants

Every flag function has a sibling that writes into a variable you already own, instead of returning a pointer. The Var variants take the address of the target variable as their first argument.

The result is the same as the pointer-returning version. The difference is ergonomic: with the Var form you work with plain values instead of dereferencing pointers everywhere, which is nicer when you're passing the values into other functions or storing them on a struct.

PatternWhen to prefer
p := flag.String(...) then *pQuick scripts, small programs, you want the pointer for nil checks
flag.StringVar(&v, ...)Larger programs, values are fields on a config struct, you want plain values

A common idiom in production CLIs is to keep all flag-backed values on a single config struct, populated through Var calls:

The config struct is easy to pass around, easy to log, and easy to test. The flag registration is the only code that has to know that these values came from the command line.

flag.Parse and Argument Syntax

flag.Parse reads os.Args[1:], matches each token against the flags you declared, fills in the values, and stops at the first non-flag argument or the special token --. If you forget to call it, every flag keeps its default value.

The diagram shows the data flow: Parse walks the argument list, separates flags from non-flag positional arguments, and routes each piece to either a registered flag's value or to the leftover-args list that flag.Args exposes.

Go's flag parser accepts a few different syntaxes for the same thing:

SyntaxExampleNotes
-flag value-threshold 5Space between name and value
-flag=value-threshold=5Equals sign, no space
--flag value--threshold 5Double-dash also accepted
--flag=value--threshold=5Double-dash with equals
-flag (bool only)-verboseSets bool to true
-flag=false-verbose=falseExplicitly set a bool to false

The double-dash form exists because users coming from GNU-style CLIs expect it. Go treats -flag and --flag identically; there's no notion of short flags vs long flags built into the package.

Boolean flags have one quirk worth knowing. You can write -verbose and the parser sets it to true, but you cannot write -verbose true with a space. The next token would be treated as a positional argument. To explicitly set a bool, use -verbose=true or -verbose=false.

The bare -dry-run form would just set it to true, which is already the default, so the explicit =false is the only way to flip a default-true bool from the command line.

Positional Arguments: flag.Args and flag.NArg

Anything left after the parser hits a non-flag token is available through flag.Args (the full slice) and flag.NArg (the count). This is how you accept positional arguments after the options.

A common CLI shape is tool [flags] <subcommand> [args...] or tool [flags] <file>. In both cases, positional arguments after the flags are how you express what the tool should act on, while the flags configure how.

The -- separator forces an end to flag parsing. Anything after it is treated as positional even if it starts with a dash:

This is handy when a positional argument might legitimately start with a dash, like a product name "-Best Seller-" or a path that begins with one.

Customizing flag.Usage

When the user passes -help, -h, or an unknown flag, the package prints a usage message and exits. The default message is a header followed by one line per flag. You can replace it by assigning a function to flag.Usage.

The trick is calling flag.PrintDefaults from inside your custom function. That's what generates the per-flag lines with names, types, usage strings, and defaults. Anything you put around that call becomes the surrounding context.

The package writes help output to os.Stderr by default. If you want it on os.Stdout (which is more conventional for -help), point flag.CommandLine.SetOutput at stdout:

That changes both the help message and parse-error output. Most modern CLIs do this so users can pipe tool -help into less or grep.

Subcommands with flag.NewFlagSet

The standard library has no built-in subcommand support. There's no flag.AddSubcommand or anything like it. What you do instead is dispatch by the first positional argument and let each subcommand parse its own FlagSet.

A FlagSet is just a named, independent set of flags. The package-level flag.String, flag.Int, and flag.Parse calls all act on a default FlagSet called flag.CommandLine. You can make your own with flag.NewFlagSet, register flags on it, and call Parse on it with whatever slice of arguments you want.

The diagram shows the dispatch tree: read the first non-program argument, choose a subcommand, hand the rest of the arguments to that subcommand's FlagSet, then run the matching handler. Each subcommand gets its own flag namespace, so inventory list -format json and inventory report -format json can both use -format without conflict.

The inventory CLI below has three subcommands.

Each NewFlagSet call takes a name (used in error messages) and an error-handling mode. flag.ExitOnError makes the FlagSet call os.Exit(2) on a parse error, which is what most CLIs want. flag.ContinueOnError returns the error from Parse so you can handle it yourself, useful in tests where you don't want the process to exit.

Each subcommand reads its arguments from os.Args[2:], which is everything after the program name and the subcommand. That slice is what gets passed to fs.Parse. If you want flags between the program name and the subcommand (so users can write inventory -verbose list), you'd call flag.Parse() on the default FlagSet first, then dispatch on flag.Arg(0) and pass flag.Args()[1:] to the subcommand. Most tools pick one or the other and stick with it.

Custom flag.Value Types

Sometimes a flag's value doesn't fit any of the built-in types. You want to accept a comma-separated list of categories, or restrict a flag to a fixed set of enum values, or parse a custom date format. The flag.Value interface is how you plug in your own parser.

The interface is small:

Implement those two methods on a type, and you can pass a pointer to a value of that type to flag.Var. The String method returns the current value as a string (used in help output and error messages). The Set method is called once per occurrence of the flag, with the string the user typed.

A flag that accepts a comma-separated list of category names:

The flag can be repeated, because Set is called once per occurrence and appends to the slice each time. Each occurrence can itself be comma-separated, because Set splits on commas. Users can write whichever style they prefer.

Enum-style flags are another common case. A -format flag that only accepts text, json, or csv:

A bad value is rejected during parsing. The error message comes from your Set method, wrapped by the flag package with the flag name. The parser exits with status 2 because the default FlagSet uses flag.ExitOnError.

Environment Variable Fallback

A common pattern is to let users set a flag through an environment variable when they don't pass it on the command line. The flag package doesn't do this for you, but it's a few lines.

The cleanest version reads the env var to compute the default, then declares the flag with that default. The user can still override with the flag:

The flag wins when both are set, which is the convention most CLIs follow: env vars are the default, flags are the override. Mentioning the env var in the usage string is a habit worth picking up; users reading -help should not have to guess.

A second pattern walks the registered flags after parsing and fills in any unset ones from the environment. flag.Visit walks only flags that were actually set on the command line, while flag.VisitAll walks every registered flag. The difference lets you detect which flags the user passed.

The pattern keeps env-var support DRY: one loop covers every flag, and the help string doesn't have to repeat the env name.

When the Stdlib Is Enough

The flag package covers a real range of CLI tools without help: typed flags, positional arguments, customizable help, manual subcommand dispatch, custom value types. For a single-purpose tool, or a tool with two or three subcommands, this is enough and the lack of dependencies is a feature.

A few popular alternatives show up when CLIs grow:

LibraryStrengthsWhen to consider
spf13/cobraNested subcommands, generated help, shell completion, used by kubectl and hugoCLIs with many commands or a need for autocompletion
urfave/cliSimpler than cobra, fluent API, decent help outputMedium CLIs where cobra feels heavyweight
alecthomas/kongStruct-tag-driven, type-safe, less boilerplateWhen you want flags declared as struct fields with tags
spf13/pflagGNU-style short/long flags, used internally by cobraWhen you specifically need -v short flags alongside --verbose

Use one of those when you have nested subcommands (tool service deploy --env prod), when you want generated shell completion, or when the help output starts to feel like more code than your actual commands. Stay with flag when the tool is small, when adding a dependency to a single binary feels heavy, or when you want to keep the build closed to the standard library.

One common mistake is to start with flag, hit a limit, and then sprinkle ad-hoc parsing on top. If you're writing custom code to handle two levels of subcommands or your own short-flag aliasing, you've left the territory where flag is shorter than cobra. Switching libraries early, before that code accumulates, is usually cheaper.

What Goes Wrong

A short tour of the common ways CLI programs break when using the flag package.

Forgetting to call `flag.Parse`. Every flag stays at its default. The program runs as if the user passed no arguments. There's no error and no warning. The fix is one line, but the bug can survive a code review because the code "looks right".

Reading the pointer before `Parse`. Same problem with a different shape. The value is zero (or whatever the default was) at the time you read it. Always parse first, use the values after.

Declaring flags inside a function that runs after `Parse`. A flag registered after Parse has been called isn't part of the parse. It always keeps its default. Declare flags at the top of main or in init, before any Parse call.

Bool flag with a positional value. Writing -verbose true makes verbose true and pushes true into flag.Args(). The user thinks they set the value, the program thinks they passed a positional. Use -verbose=true or just -verbose.

Calling `flag.Parse` from a library. Libraries should never touch the global flag.CommandLine or call flag.Parse. That belongs to the program's main. A library that wants its own flags should use flag.NewFlagSet and accept the args explicitly, or expose a config struct and let the caller wire it up.

Putting It All Together

The inventory CLI below pulls in most of what this chapter covered: a config struct, custom Usage, subcommand dispatch, a custom Value type for the category list, and env-var fallback for the store ID.

Everything in this program is the standard library. No third-party CLI framework, no code generation, no magic. The whole thing fits in one file and reads top to bottom, which is exactly the situation flag was designed for.

Quiz

flag Package Quiz

10 quizzes