flag Package in Depth

Updated

September 8, 2026

flag Package in Depth

Overview

Package flag is the stdlib CLI parser: bool/int/string/duration flags, custom Value types, and usage text. Master this before Cobra—Cobra’s flags are powered by pflag, a POSIX-friendly fork of the same ideas.

Minimal example

package main

import (
    "flag"
    "fmt"
    "os"
    "time"
)

func main() {
    var (
        name    = flag.String("name", "world", "name to greet")
        times   = flag.Int("n", 1, "repeat count")
        verbose = flag.Bool("v", false, "verbose")
        timeout = flag.Duration("timeout", 2*time.Second, "budget")
    )
    flag.Parse()

    if *verbose {
        fmt.Fprintf(os.Stderr, "timeout=%s args=%v\n", *timeout, flag.Args())
    }
    for i := 0; i < *times; i++ {
        fmt.Printf("hello, %s\n", *name)
    }
}
go run . -name Ada -n 2 -v -- leftover
# flags after -- are not parsed as flags in some styles;
# with flag, non-flag args are flag.Args() after Parse

Var forms (bind to your own vars)

var cfg struct {
    Addr string
    Port int
}
flag.StringVar(&cfg.Addr, "addr", "127.0.0.1", "listen address")
flag.IntVar(&cfg.Port, "port", 8080, "port")
flag.Parse()

Prefer Var when flags map into a config struct.

Custom Usage

flag.Usage = func() {
    fmt.Fprintf(flag.CommandLine.Output(),
        "Usage: %s [flags] <path>\n\nFlags:\n", filepath.Base(os.Args[0]))
    flag.PrintDefaults()
}

flag.CommandLine.Output() defaults to stderr—keep it that way.

Error handling modes

fs := flag.NewFlagSet("tool", flag.ContinueOnError)
// flag.ExitOnError  — print usage, os.Exit(2)  (CommandLine default)
// flag.PanicOnError — panic (rare)
// flag.ContinueOnError — return error from Parse (best for tests)

For testable tools:

fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
fs.SetOutput(io.Discard) // or a buffer when asserting usage
err := fs.Parse(args)

Custom flag types (flag.Value)

type stringsFlag []string

func (s *stringsFlag) String() string { return fmt.Sprint(*s) }
func (s *stringsFlag) Set(v string) error {
    *s = append(*s, v)
    return nil
}

func main() {
    var tags stringsFlag
    flag.Var(&tags, "tag", "repeatable tag (use multiple -tag)")
    flag.Parse()
    fmt.Println(tags)
}
go run . -tag a -tag b
# [a b]

Enum flag

type level string

func (l *level) String() string { return string(*l) }
func (l *level) Set(v string) error {
    switch v {
    case "debug", "info", "error":
        *l = level(v)
        return nil
    default:
        return fmt.Errorf("level must be debug|info|error")
    }
}

var lvl level = "info"
flag.Var(&lvl, "level", "log level")

Bool flags

verbose := flag.Bool("verbose", false, "verbose")
// -verbose, -verbose=true, -verbose=false
// short -v needs a separate flag or pflag/Cobra
flag.Bool("v", false, "verbose (short)")

Stdlib flag does not combine short options (-abc). Use pflag/Cobra for GNU style.

Positional args after flags

flag.Parse()
paths := flag.Args()
if len(paths) == 0 {
    fmt.Fprintln(os.Stderr, "need at least one path")
    os.Exit(2)
}

Order: users typically pass flags first. With stdlib flag, a non-flag stops flag parsing for the rest (classic Go behavior).

Example: httpget (stdlib-only client)

func main() {
    url := flag.String("url", "", "URL to GET")
    timeout := flag.Duration("timeout", 10*time.Second, "client timeout")
    flag.Parse()
    if *url == "" {
        flag.Usage()
        os.Exit(2)
    }
    client := &http.Client{Timeout: *timeout}
    resp, err := client.Get(*url)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    defer resp.Body.Close()
    fmt.Println(resp.Status)
    _, _ = io.Copy(os.Stdout, io.LimitReader(resp.Body, 1<<20))
}

Example: sleepfor

d := flag.Duration("d", time.Second, "sleep duration")
flag.Parse()
time.Sleep(*d)

Example: lines — count lines in files

func main() {
    verbose := flag.Bool("v", false, "print per-file counts")
    flag.Parse()
    total := 0
    for _, path := range flag.Args() {
        n, err := countLines(path)
        if err != nil {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
        if *verbose {
            fmt.Printf("%d\t%s\n", n, path)
        }
        total += n
    }
    fmt.Println(total)
}

Rules of thumb

Do Don’t
Document every flag default in help Undocumented env-only secret flags as the only API
ContinueOnError + run() for tests Parse global flag in init of libraries
Custom Value for repeatable/enum Parse enums with silent fallbacks

Try next

  1. Add a repeatable -header k=v flag using flag.Value.
  2. Write tests that parse a private FlagSet with ContinueOnError.
  3. Build greet -n 3 -name X and verify usage on bad -n=abc.