How to use flag package
You're building a command-line tool to process log files. Hardcoding the input path works for your laptop, but breaks the moment you share the script. You need a way to pass configuration from the terminal without wrestling with a third-party dependency. Go ships with flag in the standard library. It handles parsing arguments, generating help text, and type conversion out of the box. No go get required.
The flag package connects command-line arguments to Go variables. You define a variable, tell flag which switch name maps to it, provide a default value, and write a help string. When the program runs, flag scans the arguments, converts strings to the correct types, and updates your variables. If the user passes a string where an integer is expected, flag catches the error before your code runs. Think of it as a type-safe bridge between the terminal and your memory.
Here's the simplest usage: bind a string, parse, print.
package main
import (
"flag"
"fmt"
)
// main starts the CLI and prints a greeting.
func main() {
// name holds the value passed via -name, defaulting to "world".
var name string
flag.StringVar(&name, "name", "world", "A greeting name")
// Parse reads os.Args and populates the bound variables.
flag.Parse()
fmt.Println("Hello", name)
}
When you call flag.StringVar, you aren't parsing anything yet. You're registering a definition with the global flag set. The function takes a pointer to your variable so it can write the result directly into memory. The second argument is the flag name. The third is the default. The fourth is the usage string that appears in the help output.
Calling flag.Parse triggers the actual work. It iterates over os.Args, skipping the program name. It matches switches like -name=alice or -name alice. It converts the value to the expected type and writes it to the pointer you provided. Anything that isn't a flag gets collected into flag.Args. If a required flag is missing, flag doesn't panic. It uses the default. If the type conversion fails, flag prints an error message and exits with a non-zero status.
flag parses once. Calling flag.Parse multiple times is safe; it only processes arguments on the first call.
Real tools need multiple flags and validation. Here's a pattern using a struct to group configuration.
package main
import (
"flag"
"fmt"
"log"
)
// Config holds the parsed command-line settings.
type Config struct {
Input string
Count int
Verbose bool
}
// main parses flags into a config struct and validates usage.
func main() {
// cfg stores the values that flag will populate.
var cfg Config
// Bind each field to a flag with a default and help text.
flag.StringVar(&cfg.Input, "input", "", "Path to the input file")
flag.IntVar(&cfg.Count, "count", 1, "Number of iterations")
flag.BoolVar(&cfg.Verbose, "verbose", false, "Enable detailed logging")
// Parse processes os.Args and updates cfg fields.
flag.Parse()
// Validate required arguments after parsing.
if cfg.Input == "" {
log.Fatal("flag -input is required")
}
fmt.Printf("Processing %s %d times\n", cfg.Input, cfg.Count)
if cfg.Verbose {
fmt.Println("Verbose mode enabled")
}
}
The compiler catches type mismatches at definition time. If you pass a *int to flag.StringVar, the compiler rejects this with cannot use &count (variable of type *int) as *string value in argument. Runtime errors are different. If the user provides a string where an integer is expected, flag prints flag: invalid value "abc" for flag -count: parse error and exits. You don't need to handle this manually; flag manages the exit for you.
Validate requirements after parsing. Defaults are for convenience, not requirements.
Sometimes built-in types aren't enough. You can create custom flags by implementing the flag.Value interface. This interface requires two methods: Set(string) error and Type() string. The Set method parses the string value and updates your type. The Type method returns a human-readable name for the flag, used in help output.
Here's a custom flag that accepts a comma-separated list of durations.
package main
import (
"flag"
"fmt"
"strconv"
"strings"
"time"
)
// DurationSlice implements flag.Value for a list of durations.
type DurationSlice []time.Duration
// Set parses a comma-separated list of durations.
func (d *DurationSlice) Set(s string) error {
// Split the input string into individual parts.
parts := strings.Split(s, ",")
*d = make([]time.Duration, 0, len(parts))
for _, part := range parts {
// Trim whitespace and parse each duration.
dur, err := time.ParseDuration(strings.TrimSpace(part))
if err != nil {
return fmt.Errorf("invalid duration %q: %w", part, err)
}
*d = append(*d, dur)
}
return nil
}
// Type returns the name of the custom flag type.
func (d *DurationSlice) Type() string {
return "durationSlice"
}
// main demonstrates registering and using a custom flag type.
func main() {
// delays holds the parsed list of durations.
var delays DurationSlice
// Var binds the custom type to the flag using its interface.
flag.Var(&delays, "delays", "Comma-separated list of durations (e.g., 1s,200ms)")
flag.Parse()
fmt.Println("Delays:", delays)
}
Custom types keep parsing logic out of main. Implement flag.Value and let the standard library handle the rest.
The flag package uses a global flag set by default, accessible as flag.CommandLine. This works perfectly for simple tools. It causes problems when you write libraries or tools with subcommands. If two packages both define flags, they collide in the global set. The solution is flag.NewFlagSet. It creates an isolated flag set that you can parse independently.
package main
import (
"flag"
"fmt"
)
// main creates an isolated flag set for subcommand-like behavior.
func main() {
// fs is a new flag set, separate from the global CommandLine.
fs := flag.NewFlagSet("server", flag.ExitOnError)
// port binds to the local set, not the global one.
var port int
fs.IntVar(&port, "port", 8080, "Port to listen on")
// Parse parses only the arguments passed to this set.
fs.Parse([]string{"-port", "9090"})
fmt.Println("Port:", port)
}
Use flag.NewFlagSet when you need isolation. The global set is convenient until it isn't.
If you access flag.Args before calling flag.Parse, you get a panic. The package requires parsing to happen first so it can separate flags from positional arguments. If you pass an unknown flag, flag prints flag: help requested if the flag is -h or -help, or flag: looking for argument -x if it's something else. The behavior is consistent: flag handles user errors by printing diagnostics and exiting.
Global flags are convenient until they aren't. Isolate when you scale.
Use flag when you are writing a simple CLI tool and want zero dependencies. Use flag.NewFlagSet when you need subcommands or want to isolate flag parsing from the global state. Use a third-party library like cobra or urfave/cli when you need nested subcommands, auto-generated man pages, or complex configuration merging. Use plain os.Args iteration when you are writing a library function that must not depend on command-line parsing logic.
Standard library wins for simplicity. Reach for batteries when complexity grows.