OS, Args, and Flags
OS, Args, and Flags
os is the thin layer between Go and the operating system. flag is the standard-library argument parser. The boring default is: flag for named flags, os.Args[1:] only when flags are too much, os.Getenv for configuration, and os.Exit(1) on a fatal error — after printing to os.Stderr, not os.Stdout.
Mental model
os.Args is the raw []string of the command line, os.Args[0] being the program name. flag wraps that slice with typed bindings: flag.String, flag.Int, flag.Bool. Call flag.Parse() once; after that the pointers hold the parsed values.
os.Getenv("KEY") returns a string; an absent key returns "". os.LookupEnv("KEY") returns (value, present) when you must distinguish absent from empty.
os.Exit(code) terminates the process immediately — deferred calls do not run. Reserve it for main and for top-level error paths where cleanup is not relevant. Libraries must never call it.
os.Stdin, os.Stdout, os.Stderr are open *os.File values. Pass them into functions as io.Reader or io.Writer so tests can substitute a bytes.Buffer.
Worked examples
Case 1: os.Args
Save as raw_args.go. Print every argument after the program name.
// raw_args.go
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: raw_args <name>")
os.Exit(1)
}
for i, a := range os.Args[1:] {
fmt.Printf("arg %d: %s\n", i, a)
}
}Run:
go run raw_args.go soup coffee cakeOutput:
arg 0: soup
arg 1: coffee
arg 2: cake
os.Args[1:] skips the program name. When no arguments arrive, print a usage message on os.Stderr and exit 1. Exit 0 is success; any other code signals failure to the shell.
Case 2: flag — named flags
Save as open_desk.go. The desk operator names a window and a maximum table count.
// open_desk.go
package main
import (
"flag"
"fmt"
"os"
)
func main() {
window := flag.String("window", "A", "desk window label")
maxTables := flag.Int("tables", 10, "maximum tables to seat")
flag.Parse()
if *window == "" {
fmt.Fprintln(os.Stderr, "window must not be empty")
os.Exit(1)
}
fmt.Printf("opening window %s with %d tables\n", *window, *maxTables)
}Run:
go run open_desk.go -window B -tables 14Output:
opening window B with 14 tables
flag.String returns a pointer. Dereference with *window. The flag package writes usage to os.Stderr and exits 2 on a parse error — you do not need to catch that yourself.
To see the generated help:
go run open_desk.go -helpUsage of /tmp/…:
-tables int
maximum tables to seat (default 10)
-window string
desk window label (default "A")
Case 3: flag.Args() — positional after flags
Save as print_tickets.go. Flags come first; ticket IDs follow as positional arguments.
// print_tickets.go
package main
import (
"flag"
"fmt"
"os"
)
func main() {
window := flag.String("window", "A", "desk window")
flag.Parse()
ids := flag.Args() // everything after the last flag
if len(ids) == 0 {
fmt.Fprintln(os.Stderr, "provide at least one ticket id")
os.Exit(1)
}
for _, id := range ids {
fmt.Printf("[%s] printing ticket %s\n", *window, id)
}
}Run:
go run print_tickets.go -window B 7 8 9Output:
[B] printing ticket 7
[B] printing ticket 8
[B] printing ticket 9
flag.Args() returns the non-flag arguments after flag.Parse(). Flags must precede positional arguments or the parser stops at the first non-flag token.
Case 4: os.Getenv and os.LookupEnv
Save as env_config.go. The desk zone comes from the environment; an absent key is an error.
// env_config.go
package main
import (
"fmt"
"os"
)
func main() {
zone, ok := os.LookupEnv("DESK_ZONE")
if !ok {
fmt.Fprintln(os.Stderr, "DESK_ZONE not set")
os.Exit(1)
}
fmt.Println("zone:", zone)
lang := os.Getenv("LANG")
if lang == "" {
lang = "en_US.UTF-8"
}
fmt.Println("lang:", lang)
}Run:
DESK_ZONE=EU go run env_config.goOutput:
zone: EU
lang: en_US.UTF-8
LookupEnv is correct when an empty-string value is valid but an absent key is not. Getenv returning "" cannot tell you which case you are in. Use it only when the default for both absent and empty is the same.
Case 5: Stderr and structured errors
Save as desk_cmd.go. A command that writes its log to stderr and its product to stdout so they can be piped independently.
// desk_cmd.go
package main
import (
"flag"
"fmt"
"os"
"strconv"
)
func run(w *string, ids []string, out, errOut *os.File) int {
for _, raw := range ids {
n, err := strconv.Atoi(raw)
if err != nil {
fmt.Fprintf(errOut, "invalid id %q: %v\n", raw, err)
return 1
}
fmt.Fprintf(out, "[%s] ticket %d ready\n", *w, n)
}
return 0
}
func main() {
window := flag.String("window", "A", "desk window")
flag.Parse()
code := run(window, flag.Args(), os.Stdout, os.Stderr)
os.Exit(code)
}Run:
go run desk_cmd.go -window C 7 8 badOutput:
[C] ticket 7 ready
[C] ticket 8 ready
invalid id "bad": strconv.Atoi: parsing "bad": invalid syntax
Exit code is 1. The caller’s shell can check $?. Separating stdout (data) and stderr (log/error) lets the caller pipe the data without the errors mixing in.
The trap
Save as exit_defer.go. os.Exit skips deferred calls.
// exit_defer.go
package main
import (
"fmt"
"os"
)
func main() {
defer fmt.Println("this never runs")
fmt.Println("before exit")
os.Exit(1)
}Run:
go run exit_defer.goOutput:
before exit
Exit code 1, and defer did not fire. This is by design. Do not rely on deferred cleanup in a path that calls os.Exit. Return error values up to main, do the cleanup, then exit.
The boring rule
flagfor named options.flag.Args()for positional arguments after flags.os.LookupEnvwhen absence and empty are different.os.Getenvwhen both map to the same default.- Errors go to
os.Stderr. Program output goes toos.Stdout. They are different. os.Exit(1)only inmain(or the outermostrunfunction). Libraries return errors.- Deferred calls do not run after
os.Exit. Structure cleanup before you exit.
Try this
- In
open_desk.go, add a-verbosebool flag. When true, print extra detail (any extra line you like). - In
print_tickets.go, add validation: reject any ticket id that is not a number (usestrconv.Atoi). - In
env_config.go, run withoutDESK_ZONE=. Observe the exit message on stderr. - In
desk_cmd.go, move theos.Exitinto a separatefunc main()that callsrunand exits with its return value. Confirm that removingos.Exitfromrunlets you testrunwithout killing the process.