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:#f3e5f5

Basic 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:#fff3e0
func 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 of main().
  • flag.StringVar, flag.IntVar, etc. for filling directly into existing variables — cleaner than storing pointers from flag.String() etc.
  • flag.Usage can be overridden with a custom function for more informative, branded help messages.
  • flag.FlagSet for subcommands — each subcommand has its own flag set parsed from os.Args[2:] (or the relevant slice).
  • Combine with environment variables using the envOr pattern — CLI flags can override the env, but the env provides more flexible defaults than hardcoded values.
  • Custom flag.Value for types not supported natively — implement String() and Set(string) error for any type like slices, enums, or custom structs.
  • Validate after flag.Parse() — check the parsed flag values to ensure valid flag combinations (e.g. -tls without -cert and -key).
  • flag.Args() to access non-flag arguments after parsing — useful for positional arguments like file names.
  • flag.ErrHelp when using ContinueOnError — detect -help requests and handle them explicitly for full control over the output.

← Previous: Testing   Next: Sync Atomic →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact