Flag #
Almost every Go program running from the command line needs a way to accept configuration from users — whether it’s the port to use, the config file to read, or the debug mode to enable. The flag package provides simple, idiomatic command line argument parsing: define your flags, call flag.Parse(), and the values are available as pointers or variables. This package is deliberately minimal — it doesn’t support nested subcommands or a --flag format exclusively (it supports both -flag and --flag with one or two dashes), but for simple to medium tooling needs, it’s more than enough. For more complex CLIs, FlagSet enables subcommand implementations, and combining with environment variables makes flexible configuration.
An Overview of the flag Package #
flowchart TD
F["package flag"] --> Define["Defining Flags"]
F --> Parse["Parsing"]
F --> Access["Accessing Values"]
F --> Custom["Custom Types"]
F --> FlagSet["FlagSet\nfor subcommands"]
Define --> D1["flag.String(name, default, usage)\nreturns *string"]
Define --> D2["flag.Int(name, default, usage)\nreturns *int"]
Define --> D3["flag.Bool(name, default, usage)\nreturns *bool"]
Define --> D4["flag.Duration(name, default, usage)\nreturns *time.Duration"]
Define --> D5["flag.StringVar(&var, name, default, usage)\ndirectly into a variable"]
Parse --> P1["flag.Parse()\nparse os.Args[1:]"]
Parse --> P2["flag.Args()\nnon-flag arguments after parsing"]
Parse --> P3["flag.NArg()\nthe number of non-flag arguments"]
Access --> A1["*flagName — dereference the pointer"]
Access --> A2["flag.Lookup(name)\nfind an already-defined flag"]
FlagSet --> FS1["flag.NewFlagSet(name, errorHandling)"]
FlagSet --> FS2["fs.Parse(args)\nparse a specific argument slice"]
style F fill:#4f86c6,color:#fff
style Define fill:#e8f5e9
style Parse fill:#e3f2fd
style Access fill:#fff3e0
style FlagSet fill:#f3e5f5Basic Flags #
package main
import (
"flag"
"fmt"
"os"
)
func main() {
// Define flags — returns pointers to values
host := flag.String("host", "localhost", "server host")
port := flag.Int("port", 8080, "server port")
debug := flag.Bool("debug", false, "enable debug mode")
timeout := flag.Duration("timeout", 30*time.Second, "connection timeout")
config := flag.String("config", "", "path to the config file")
// Alternative: directly into an existing variable
var verbose bool
flag.BoolVar(&verbose, "verbose", false, "verbose output")
var maxConn int
flag.IntVar(&maxConn, "max-conn", 10, "maximum number of connections")
// Parse arguments from os.Args[1:]
// MUST be called after all flags are defined, before they're used
flag.Parse()
// Access values by dereferencing the pointers
fmt.Printf("Host: %s\n", *host)
fmt.Printf("Port: %d\n", *port)
fmt.Printf("Debug: %v\n", *debug)
fmt.Printf("Timeout: %v\n", *timeout)
fmt.Printf("Verbose: %v\n", verbose)
fmt.Printf("MaxConn: %d\n", maxConn)
// Non-flag arguments (after all flags)
// Example: ./app -port 9090 file1.txt file2.txt
args := flag.Args() // ["file1.txt", "file2.txt"]
fmt.Printf("Arguments: %v\n", args)
fmt.Printf("Argument count: %d\n", flag.NArg())
}
Supported Flag Formats #
# Valid formats — all equivalent
./app -host localhost
./app --host localhost
./app -host=localhost
./app --host=localhost
# Boolean flags — both equivalent
./app -debug
./app -debug=true
./app -debug=false
# Flag order doesn't matter
./app -port 9090 -host example.com -debug
# Non-flag arguments at the end
./app -port 9090 file1.txt file2.txt
# flag.Args() = ["file1.txt", "file2.txt"]
# -- stops flag parsing
./app -port 9090 -- -this-is-not-a-flag.txt
# flag.Args() = ["-this-is-not-a-flag.txt"]
Usage — Help Messages #
func main() {
// Customize the usage message
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [flags] [file...]\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "A simple HTTP server.\n\n")
fmt.Fprintf(os.Stderr, "Flags:\n")
flag.PrintDefaults() // print all flags with the standard format
fmt.Fprintf(os.Stderr, "\nExamples:\n")
fmt.Fprintf(os.Stderr, " %s -port 9090 -debug\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s -config /etc/myapp/config.yaml\n", os.Args[0])
}
host := flag.String("host", "0.0.0.0", "the `host` address to listen on")
port := flag.Int("port", 8080, "the `port` number (1-65535)")
flag.Parse()
_ = host
_ = port
}
// Output from -help or -h:
// Usage: ./myapp [flags] [file...]
//
// A simple HTTP server.
//
// Flags:
// -host host
// the host address to listen on (default "0.0.0.0")
// -port port
// the port number (1-65535) (default 8080)
//
// Examples:
// ./myapp -port 9090 -debug
// ./myapp -config /etc/myapp/config.yaml
Custom Flag Types #
For types not supported natively (besides string, int, bool, float64, duration), implement the flag.Value interface:
// flag.Value interface:
// type Value interface {
// String() string — the default value and display at -help
// Set(string) error — set the value from an argument string
// }
// Example 1: a flag for a string slice (repeated values)
type StringSlice []string
func (ss *StringSlice) String() string {
return strings.Join(*ss, ", ")
}
func (ss *StringSlice) Set(s string) error {
*ss = append(*ss, s)
return nil
}
// Usage:
// ./app -tag backend -tag api -tag v2
// tags will contain ["backend", "api", "v2"]
// Example 2: a flag for the log level
type LogLevel int
const (
LevelDebug LogLevel = iota
LevelInfo
LevelWarn
LevelError
)
func (l *LogLevel) String() string {
switch *l {
case LevelDebug:
return "debug"
case LevelInfo:
return "info"
case LevelWarn:
return "warn"
case LevelError:
return "error"
default:
return "unknown"
}
}
func (l *LogLevel) Set(s string) error {
switch strings.ToLower(s) {
case "debug":
*l = LevelDebug
case "info":
*l = LevelInfo
case "warn", "warning":
*l = LevelWarn
case "error":
*l = LevelError
default:
return fmt.Errorf("invalid level: %q (valid: debug, info, warn, error)", s)
}
return nil
}
// Registering custom flags
func main() {
var tags StringSlice
flag.Var(&tags, "tag", "tag for filtering (can be repeated)")
var logLevel LogLevel = LevelInfo // default Info
flag.Var(&logLevel, "log-level", "logging level (debug|info|warn|error)")
flag.Parse()
fmt.Println("Tags:", tags)
fmt.Println("Log level:", logLevel)
}
FlagSet — Subcommands #
flag.FlagSet allows defining flags per subcommand — each subcommand has its own separate set of flags:
flowchart TD
App["./myapp"] --> Args["os.Args"]
Args --> Sub{subcommand?}
Sub -- "serve" --> ServFS["serveFlagSet\n -port int\n -host string\n -tls bool"]
Sub -- "migrate" --> MigrFS["migrateFlagSet\n -db string\n -dry-run bool\n -steps int"]
Sub -- "export" --> ExpFS["exportFlagSet\n -format string\n -output string\n -compress bool"]
Sub -- "help / other" --> Help["Show usage"]
ServFS --> ServeCmd["runServer()"]
MigrFS --> MigrCmd["runMigration()"]
ExpFS --> ExpCmd["runExport()"]
style App fill:#4f86c6,color:#fff
style ServFS fill:#e8f5e9
style MigrFS fill:#e3f2fd
style ExpFS fill:#fff3e0func main() {
// Check the subcommand
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "Usage: %s <subcommand> [flags]\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Available subcommands:\n")
fmt.Fprintf(os.Stderr, " serve — run the HTTP server\n")
fmt.Fprintf(os.Stderr, " migrate — run database migrations\n")
fmt.Fprintf(os.Stderr, " export — export data\n")
os.Exit(1)
}
switch os.Args[1] {
case "serve":
runServe(os.Args[2:])
case "migrate":
runMigrate(os.Args[2:])
case "export":
runExport(os.Args[2:])
case "help", "-help", "--help", "-h":
// Show the general help
fmt.Println("Use: myapp <subcommand> -help for help")
default:
fmt.Fprintf(os.Stderr, "Unknown subcommand: %q\n", os.Args[1])
os.Exit(1)
}
}
func runServe(args []string) {
// FlagSet for the serve subcommand
fs := flag.NewFlagSet("serve", flag.ExitOnError)
host := fs.String("host", "0.0.0.0", "host address")
port := fs.Int("port", 8080, "port number")
tls := fs.Bool("tls", false, "enable TLS")
certFile := fs.String("cert", "", "path to the TLS certificate")
keyFile := fs.String("key", "", "path to the TLS private key")
// Customize the usage for this subcommand
fs.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: myapp serve [flags]\n\nFlags:\n")
fs.PrintDefaults()
}
// Parse the args for this subcommand only
if err := fs.Parse(args); err != nil {
os.Exit(1)
}
// Validate
if *tls && (*certFile == "" || *keyFile == "") {
fmt.Fprintf(os.Stderr, "Error: -cert and -key are required if -tls is enabled\n")
os.Exit(1)
}
fmt.Printf("Running server at %s:%d (TLS: %v)\n", *host, *port, *tls)
}
func runMigrate(args []string) {
fs := flag.NewFlagSet("migrate", flag.ExitOnError)
dsn := fs.String("db", os.Getenv("DATABASE_URL"), "database DSN")
dryRun := fs.Bool("dry-run", false, "show the SQL without executing it")
steps := fs.Int("steps", 0, "number of migrations (0 = all)")
fs.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: myapp migrate [flags]\n\nFlags:\n")
fs.PrintDefaults()
}
fs.Parse(args)
if *dsn == "" {
fmt.Fprintf(os.Stderr, "Error: -db or DATABASE_URL is required\n")
os.Exit(1)
}
fmt.Printf("Database migration: %s (dry-run: %v, steps: %d)\n",
*dsn, *dryRun, *steps)
}
func runExport(args []string) {
fs := flag.NewFlagSet("export", flag.ExitOnError)
format := fs.String("format", "json", "output format (json|csv|xlsx)")
output := fs.String("output", "-", "output file (- for stdout)")
compress := fs.Bool("compress", false, "compress the output with gzip")
fs.Parse(args)
fmt.Printf("Export: format=%s, output=%s, compress=%v\n",
*format, *output, *compress)
}
Integration with Environment Variables #
A very common pattern in production applications is supporting configuration from both — CLI flags for interactive use, environment variables for automated deployment:
flowchart LR
subgraph Priority["Configuration Priority (high to low)"]
P1["1. CLI flags\n(-port 9090)"]
P2["2. Environment variables\n(PORT=9090)"]
P3["3. Config files\n(config.yaml)"]
P4["4. Default values\n(port: 8080)"]
end
P1 --> P2 --> P3 --> P4
style P1 fill:#e8f5e9
style P2 fill:#e3f2fd
style P3 fill:#fff3e0
style P4 fill:#f3e5f5// Helper: take from the env or use a default
func envOr(key, defaultVal string) string {
if val := os.Getenv(key); val != "" {
return val
}
return defaultVal
}
func envOrInt(key string, defaultVal int) int {
if val := os.Getenv(key); val != "" {
n, err := strconv.Atoi(val)
if err == nil {
return n
}
}
return defaultVal
}
func envOrBool(key string, defaultVal bool) bool {
if val := os.Getenv(key); val != "" {
b, err := strconv.ParseBool(val)
if err == nil {
return b
}
}
return defaultVal
}
// Configuration supporting both
func main() {
// Defaults taken from the env, but can be overridden with flags
host := flag.String("host",
envOr("APP_HOST", "0.0.0.0"),
"server host (env: APP_HOST)")
port := flag.Int("port",
envOrInt("PORT", 8080),
"server port (env: PORT)")
debug := flag.Bool("debug",
envOrBool("APP_DEBUG", false),
"debug mode (env: APP_DEBUG)")
dbURL := flag.String("db",
envOr("DATABASE_URL", ""),
"database URL (env: DATABASE_URL)")
flag.Parse()
// Validate required values
if *dbURL == "" {
fmt.Fprintf(os.Stderr,
"Error: -db or DATABASE_URL is required\n")
flag.Usage()
os.Exit(1)
}
fmt.Printf("Configuration:\n")
fmt.Printf(" Host: %s\n", *host)
fmt.Printf(" Port: %d\n", *port)
fmt.Printf(" Debug: %v\n", *debug)
fmt.Printf(" DB: %s\n", maskDSN(*dbURL))
}
func maskDSN(dsn string) string {
// Hide the password from the DSN for logging
re := regexp.MustCompile(`://[^:]+:[^@]+@`)
return re.ReplaceAllString(dsn, "://***:***@")
}
ErrorHandling in FlagSet #
// flag.ContinueOnError — return the error, don't panic/exit
fs := flag.NewFlagSet("myapp", flag.ContinueOnError)
// flag.ExitOnError — call os.Exit(2) on errors (the flag package default)
fs2 := flag.NewFlagSet("myapp", flag.ExitOnError)
// flag.PanicOnError — panic on errors
fs3 := flag.NewFlagSet("myapp", flag.PanicOnError)
// Example of using ContinueOnError for full control
fs4 := flag.NewFlagSet("myapp", flag.ContinueOnError)
var buf bytes.Buffer
fs4.SetOutput(&buf) // redirect error output to a buffer
port := fs4.Int("port", 8080, "server port")
if err := fs4.Parse(os.Args[1:]); err != nil {
if err == flag.ErrHelp {
// The user requested -help
fmt.Println(buf.String())
os.Exit(0)
}
fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n%s", err, buf.String())
os.Exit(1)
}
_ = port
Production Usage Patterns #
A Config Struct from Flags #
// Collect all configuration in one struct
type AppConfig struct {
Host string
Port int
Debug bool
DatabaseURL string
LogLevel string
MaxWorkers int
Timeout time.Duration
Tags []string
}
func parseConfig() *AppConfig {
cfg := &AppConfig{}
// Flags with default values from the environment
flag.StringVar(&cfg.Host, "host",
envOr("HOST", "0.0.0.0"), "host address (env: HOST)")
flag.IntVar(&cfg.Port, "port",
envOrInt("PORT", 8080), "server port (env: PORT)")
flag.BoolVar(&cfg.Debug, "debug",
envOrBool("DEBUG", false), "debug mode (env: DEBUG)")
flag.StringVar(&cfg.DatabaseURL, "db",
envOr("DATABASE_URL", ""), "database URL (env: DATABASE_URL)")
flag.StringVar(&cfg.LogLevel, "log-level",
envOr("LOG_LEVEL", "info"), "log level (env: LOG_LEVEL)")
flag.IntVar(&cfg.MaxWorkers, "workers",
envOrInt("MAX_WORKERS", 4), "number of workers (env: MAX_WORKERS)")
flag.DurationVar(&cfg.Timeout, "timeout",
30*time.Second, "operation timeout")
// A custom flag for a slice
var tags StringSlice
flag.Var(&tags, "tag", "filter tag (can be repeated)")
flag.Parse()
cfg.Tags = []string(tags)
return cfg
}
func validateConfig(cfg *AppConfig) error {
var errs []string
if cfg.DatabaseURL == "" {
errs = append(errs, "database URL is required (-db or DATABASE_URL)")
}
if cfg.Port < 1 || cfg.Port > 65535 {
errs = append(errs, fmt.Sprintf("port must be 1-65535, got: %d", cfg.Port))
}
validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
if !validLevels[cfg.LogLevel] {
errs = append(errs, fmt.Sprintf("invalid log-level: %q", cfg.LogLevel))
}
if len(errs) > 0 {
return fmt.Errorf("invalid configuration:\n - %s",
strings.Join(errs, "\n - "))
}
return nil
}
func main() {
cfg := parseConfig()
if err := validateConfig(cfg); err != nil {
fmt.Fprintln(os.Stderr, err)
fmt.Fprintf(os.Stderr, "\nUse -help to see all available flags.\n")
os.Exit(1)
}
fmt.Printf("Running the application with configuration:\n")
fmt.Printf(" Host: %s:%d\n", cfg.Host, cfg.Port)
fmt.Printf(" Debug: %v\n", cfg.Debug)
fmt.Printf(" Workers: %d\n", cfg.MaxWorkers)
fmt.Printf(" Timeout: %v\n", cfg.Timeout)
fmt.Printf(" Log: %s\n", cfg.LogLevel)
}
A Complete CLI Tool #
// A file manipulation tool — an example of a complete CLI
func main() {
// Global flags
verbose := flag.Bool("v", false, "verbose output")
version := flag.Bool("version", false, "show the version")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, `Usage: filetool [flags] <subcommand> [subcommand-flags] [args]
Subcommands:
copy — copy a file
move — move a file
hash — calculate a file hash
search — search for files
Global flags:
`)
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nUse 'filetool <subcommand> -help' for subcommand help.\n")
}
// Parse with -v and -version before the subcommand
// But stop before the subcommand so its flags aren't blocked
flag.Parse()
if *version {
fmt.Println("filetool v1.0.0")
os.Exit(0)
}
args := flag.Args()
if len(args) == 0 {
flag.Usage()
os.Exit(1)
}
subcommand := args[0]
subArgs := args[1:]
switch subcommand {
case "copy":
subCopy(*verbose, subArgs)
case "move":
subMove(*verbose, subArgs)
case "hash":
subHash(*verbose, subArgs)
case "search":
subSearch(*verbose, subArgs)
default:
fmt.Fprintf(os.Stderr, "Error: unknown subcommand: %q\n\n", subcommand)
flag.Usage()
os.Exit(1)
}
}
func subHash(verbose bool, args []string) {
fs := flag.NewFlagSet("hash", flag.ExitOnError)
algo := fs.String("algo", "sha256", "hash algorithm (sha256|sha512|md5)")
fs.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: filetool hash [flags] <file...>\n\nFlags:\n")
fs.PrintDefaults()
}
fs.Parse(args)
files := fs.Args()
if len(files) == 0 {
fmt.Fprintln(os.Stderr, "Error: at least one file is required")
fs.Usage()
os.Exit(1)
}
for _, f := range files {
hash, err := calculateFileHash(f, *algo)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
continue
}
if verbose {
fmt.Printf("%s (%s): %s\n", f, *algo, hash)
} else {
fmt.Printf("%s %s\n", hash, f)
}
}
}
When to Switch to Alternatives #
Keep using flag if:
✓ Simple tools with a few flags
✓ Zero external dependencies
✓ Flags without nested subcommands
✓ Wanting something idiomatic and easy to understand
Consider external libraries if:
✗ Nested subcommands (app server start --port 8080)
→ cobra (github.com/spf13/cobra) — the most popular
→ urfave/cli — a simpler alternative
✗ Auto-generating bash/zsh completions
→ cobra supports this built-in
✗ Complex flag validation and transformation
→ kong (github.com/alecthomas/kong)
✗ Configuration from files + env + flags in one package
→ viper (github.com/spf13/viper) — often used with cobra
✗ POSIX-style flags with shorthands (-v, --verbose)
→ pflag (github.com/spf13/pflag) — used by cobra
Note: the flag package supports both -flag and --flag,
but doesn't support combined shorthands like -vdf (= -v -d -f)
Summary #
flag.Parse()must be called after all flags are defined and before flag values are accessed — usually at the start ofmain().flag.StringVar,flag.IntVar, etc. for filling directly into existing variables — cleaner than storing pointers fromflag.String()etc.flag.Usagecan be overridden with a custom function for more informative, branded help messages.flag.FlagSetfor subcommands — each subcommand has its own flag set parsed fromos.Args[2:](or the relevant slice).- Combine with environment variables using the
envOrpattern — CLI flags can override the env, but the env provides more flexible defaults than hardcoded values.- Custom
flag.Valuefor types not supported natively — implementString()andSet(string) errorfor any type like slices, enums, or custom structs.- Validate after
flag.Parse()— check the parsed flag values to ensure valid flag combinations (e.g.-tlswithout-certand-key).flag.Args()to access non-flag arguments after parsing — useful for positional arguments like file names.flag.ErrHelpwhen usingContinueOnError— detect-helprequests and handle them explicitly for full control over the output.