flag Package & POSIX-Style Flags
The Go standard library flag package parses command-line options without third-party dependencies.
It supports boolean, string, integer, duration, and custom flag.Value types, with POSIX-style -name=value and clustered boolean short flags.
For multi-command tools, dedicated flag.FlagSet instances keep subcommand flags from colliding.
Summary
flag registers variables before Parse, then mutates them from os.Args.
The default set is flag.CommandLine; production CLIs often use flag.NewFlagSet per subcommand.
Usage text, error handling, and help output are customizable so scripts and humans get predictable behavior.
Recipe
Quick-reference recipe card - copy-paste ready.
fs := flag.NewFlagSet("deploy", flag.ExitOnError)
fs.Usage = func() {
fmt.Fprintf(os.Stderr, "usage: mytool deploy [flags] <env>\n")
fs.PrintDefaults()
}
timeout := fs.Duration("timeout", 30*time.Second, "operation timeout")
_ = fs.Parse(os.Args[2:])When to reach for this:
- Single-binary tools with one command and a modest flag surface.
- Subcommands you dispatch manually with a
switchonos.Args[1]. - Tests that need an isolated flag set without touching globals.
- Libraries that should not pull cobra into every consumer.
Working Example
package main
import (
"flag"
"fmt"
"os"
"time"
)
type mode string
func (m *mode) Set(s string) error {
switch s {
case "dry", "apply":
*m = mode(s)
return nil
default:
return fmt.Errorf("mode must be dry or apply")
}
}
func (m *mode) String() string { return string(*m) }
func main() {
fs := flag.NewFlagSet("run", flag.ExitOnError)
var m mode
fs.Var(&m, "mode", "dry or apply")
verbose := fs.Bool("v", false, "verbose logging")
timeout := fs.Duration("timeout", 10*time.Second, "max wait")
fs.Usage = func() {
fmt.Fprintf(os.Stderr, "usage: %s run [flags]\n", os.Args[0])
fs.PrintDefaults()
}
if err := fs.Parse(os.Args[1:]); err != nil {
os.Exit(2)
}
fmt.Printf("mode=%s verbose=%v timeout=%s args=%v\n", m, *verbose, *timeout, fs.Args())
}What this demonstrates:
- Custom types implement
flag.ValuewithSetandString. fs.Varregisters enumerations with validation inSet.Durationflags parse Go duration strings (300ms,2m).- Usage prints to stderr with defaults for every flag.
Deep Dive
How It Works
- Registration (
String,Bool,Int,Var) records metadata in aFlagSet. Parsewalks arguments: flags start with-; the first non-flag token ends flag parsing unlessFlagSetset otherwise.- Boolean flags accept
-v,-v=true, and-v=false. - After parsing,
Args()returns remaining positional tokens.
Parsing Conventions at a Glance
| Form | Example | Notes |
|---|---|---|
| Long with value | -timeout=30s | Preferred for scripts |
| Long space-separated | -timeout 30s | Supported for non-bool flags |
| Short bool | -v | Sets true |
| Positional tail | file.txt | Available via Args() |
| Unknown flag | -zzz | Triggers Usage then exit (default set) |
FlagSet Error Modes
| Mode | Behavior |
|---|---|
flag.ExitOnError | Print error + usage, os.Exit(2) |
flag.ContinueOnError | Return parse error to caller |
flag.PanicOnError | Panic on parse error (rare in apps) |
Go Notes
// Introspect flags in tests
fs.Visit(func(f *flag.Flag) {
t.Log(f.Name, f.Value.String())
})- Avoid mutating
flag.CommandLinein libraries; export aRun(args []string)that uses a privateFlagSet. - GNU-style
--longis not native; users expect single-dash POSIX style unless you add a wrapper. - Combine with
embedor env defaults inmain, not inside reusable packages.
Gotchas
- Parsing before all flags register - Late
Stringcalls afterParsesilently miss argv. Fix: register every flag at startup beforeParse. - Global
flag.CommandLinein tests - Parallel tests fight over the same set. Fix:NewFlagSetper test case. - Missing
fs.Parse(os.Args[2:])offset - Subcommand name gets parsed as a flag value. Fix: slice args after the subcommand token. - Usage on stdout - Breaks
tool --help | greppatterns mixed with data pipes. Fix: alwaysFprintf(os.Stderr, ...). - No built-in subcommands - Manual dispatch scales poorly past three commands. Fix: migrate to cobra or urfave/cli.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| cobra / urfave/cli | Many subcommands, completion | A single -config flag is enough |
| env-only config | Containerized tools with injected env | Operators need local override files |
kong / go-flags | Struct-tag driven parsing | You want zero third-party deps |
Manual os.Args scan | Tiny scripts | Validation and help text matter |
FAQs
Does flag support --double-dash GNU style?
Not natively.
Users pass -name or -name=value.
Libraries like cobra add GNU compatibility if your audience expects it.
How do I mark a flag required?
Check after Parse: if *api == "", call Usage and exit.
cobra offers MarkFlagRequired for the same guard.
Can flags appear after positionals?
The default FlagSet stops parsing flags at the first non-flag argument.
Use a framework or custom parser if you need GNU interleaving.
How do I test parsing without os.Args?
Call fs.Parse([]string{"-v", "file"}) on a dedicated FlagSet.
Never rely on global flag.Parse in parallel tests.
What exit code does flag use on error?
ExitOnError uses exit code 2 after printing usage.
Document your own codes for business logic failures separately.
How do durations format in help?
Defaults print using the flag's String() method.
flag.Duration shows readable values like 10s.
Can one flag short name map to multiple long names?
Register two flags pointing at the same variable, or use cobra aliases.
The stdlib does not provide aliases alone.
How does flag interact with go test?
go test consumes its own flags.
Run go test ./... -- -myflag to pass flags to the test binary, or test a package function directly.
Should libraries define flags in init()?
Avoid it.
Let main own CLI wiring so importers are not surprised by global side effects.
When is flag not enough?
When you need shell completion, persistent parent flags, or auto-generated markdown help.
Upgrade to cobra or urfave/cli while keeping the same main binary.
Related
- CLI Tools Basics - introductory flag examples
- CLI Design in Go: Single Binary, Fast Startup - compile and distribution model
- cobra & urfave/cli Command Trees - subcommand frameworks
- Configuration: Viper, Env & Config Files - flags plus env and files
- CLI Tools Best Practices - testing and UX checklist
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).