Module lessons (4/5)
Mini-project: a CLI with flags
Let's put the building blocks of the previous modules together to build a small idiomatic CLI: flag parsing with the flag package, handling of positional arguments, errors on os.Stderr, and the right exit code.
The flag package
package main
import (
"flag"
"fmt"
"os"
)
func main() {
n := flag.Int("n", 1, "number of repetitions")
upper := flag.Bool("u", false, "print in uppercase")
flag.Parse()
args := flag.Args() // remaining positional arguments
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: echo [-n N] [-u] <text>")
os.Exit(2)
}
text := args[0]
if *upper {
text = strings.ToUpper(text)
}
for i := 0; i < *n; i++ {
fmt.Println(text)
}
}Key points:
flag.Int,flag.String,flag.Bool,flag.Duration(etc.) return a pointer: you use*n,*upper.flag.Parse()must be called ONCE, before reading the values and beforeflag.Args().flag.Args()returns the positional arguments (those after--or after the last flag).- Accepted forms:
-n 3,-n=3,--n=3. Booleans:-u(true),-u=false.
Conventional exit codes
os.Exit(0) // success
os.Exit(1) // generic error
os.Exit(2) // usage error (incorrect flag usage)Shells and scripts expect these codes: use them consistently. Never exit from a non-main goroutine with os.Exit: it skips every defer, including log flushing.
Stderr vs Stdout
Old and sacred Unix rule:
os.Stdout→ useful output, pipe-able (cli | grep ...).os.Stderr→ diagnostics, errors, progress bars, usage.
fmt.Println("result: 42") // stdout
fmt.Fprintln(os.Stderr, "error: ...") // stderrThis way cli 2>/dev/null shows only the results, cli >out.txt redirects only the result and errors stay visible.
log vs fmt
fmtfor user output.logfor diagnostics: adds a timestamp, writes toos.Stderrby default, haslog.Fatal(logs +os.Exit(1)) andlog.Panic.
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // prints and exits with 1For modern apps (Go 1.21+) use log/slog for structured logging (JSON or text, levels, typed fields).
Architecture: separate main from logic
func main() {
if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(args []string, stdout, stderr io.Writer) error {
// here use a new flag.FlagSet instead of the global flag.CommandLine
fs := flag.NewFlagSet("echo", flag.ContinueOnError)
fs.SetOutput(stderr)
n := fs.Int("n", 1, "repetitions")
if err := fs.Parse(args); err != nil {
return err
}
// ...
return nil
}Advantages:
- Testable: you pass
bytes.Bufferas stdout/stderr and verify the output. - No global state: each
FlagSetis isolated. mainstays tiny: it prepares IO + delegates.
Subcommands
For CLIs with subcommands (mycli serve, mycli migrate), a scheme with no external dependencies:
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
switch os.Args[1] {
case "serve":
serveCmd(os.Args[2:])
case "migrate":
migrateCmd(os.Args[2:])
default:
usage()
os.Exit(2)
}
}Each subcommand has its own flag.NewFlagSet. For larger projects, libraries like cobra or urfave/cli structure this scheme with auto-generated help.
Exercises
Definisci il flag -n di tipo int con valore di default 1 e descrizione 'ripetizioni', poi chiama flag.Parse() e stampa il valore.
Solution available after 3 attempts
Se non c'è almeno un argomento posizionale dopo flag.Parse, stampa 'manca argomento' su Stderr e termina con os.Exit(1).
Solution available after 3 attempts
Su quale stream stampi messaggi di errore e usage in un CLI Unix-friendly?
fmt.Fprintln(???, "uso: prog ...")