Lezioni del modulo (4/5)
Mini-progetto: una CLI con flag
Mettiamo insieme i mattoni dei moduli precedenti per costruire una piccola CLI idiomatica: parsing dei flag con il pacchetto flag, gestione di argomenti posizionali, errori su os.Stderr, exit code corretto.
Il pacchetto flag
package main
import (
"flag"
"fmt"
"os"
)
func main() {
n := flag.Int("n", 1, "numero di ripetizioni")
upper := flag.Bool("u", false, "stampa in maiuscolo")
flag.Parse()
args := flag.Args() // argomenti posizionali rimanenti
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "uso: echo [-n N] [-u] <testo>")
os.Exit(2)
}
text := args[0]
if *upper {
text = strings.ToUpper(text)
}
for i := 0; i < *n; i++ {
fmt.Println(text)
}
}Punti chiave:
flag.Int,flag.String,flag.Bool,flag.Duration(etc.) ritornano un puntatore: usi*n,*upper.flag.Parse()va chiamato UNA volta, prima di leggere i valori e prima diflag.Args().flag.Args()ritorna gli argomenti posizionali (quelli dopo--o dopo l'ultimo flag).- Forme accettate:
-n 3,-n=3,--n=3. Booleani:-u(true),-u=false.
Exit code convenzionali
os.Exit(0) // successo
os.Exit(1) // errore generico
os.Exit(2) // errore d'uso (uso flag scorretto)Le shell e gli script si aspettano questi codici: usali consistentemente. Mai uscire da una goroutine non-main con os.Exit: salta tutti i defer, incluso log flushing.
Stderr vs Stdout
Regola Unix antica e sacra:
os.Stdout→ output utile, pipe-abile (cli | grep ...).os.Stderr→ diagnostica, errori, progress bar, usage.
fmt.Println("risultato: 42") // stdout
fmt.Fprintln(os.Stderr, "errore: ...") // stderrCosì cli 2>/dev/null mostra solo i risultati, cli >out.txt dirotta solo il risultato e gli errori restano visibili.
log vs fmt
fmtper output utente.logper diagnostica: aggiunge timestamp, scrive di default suos.Stderr, halog.Fatal(logga +os.Exit(1)) elog.Panic.
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // stampa e esce con 1Per app moderne (Go 1.21+) usa log/slog per logging strutturato (JSON o testo, livelli, campi tipati).
Architettura: separare main dalla logica
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 {
// qui usa un nuovo flag.FlagSet invece del globale flag.CommandLine
fs := flag.NewFlagSet("echo", flag.ContinueOnError)
fs.SetOutput(stderr)
n := fs.Int("n", 1, "ripetizioni")
if err := fs.Parse(args); err != nil {
return err
}
// ...
return nil
}Vantaggi:
- Testabile: passi
bytes.Buffercome stdout/stderr e verifichi l'output. - Niente stato globale: ogni
FlagSetè isolato. mainresta minuscolo: prepara IO + delega.
Sotto-comandi
Per CLI con sotto-comandi (mycli serve, mycli migrate), uno schema senza dipendenze esterne:
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)
}
}Ogni sotto-comando ha il proprio flag.NewFlagSet. Per progetti più grandi, librerie come cobra o urfave/cli strutturano questo schema con help auto-generato.
Esercizi
Definisci il flag -n di tipo int con valore di default 1 e descrizione 'ripetizioni', poi chiama flag.Parse() e stampa il valore.
Soluzione disponibile dopo 3 tentativi
Se non c'è almeno un argomento posizionale dopo flag.Parse, stampa 'manca argomento' su Stderr e termina con os.Exit(1).
Soluzione disponibile dopo 3 tentativi
Su quale stream stampi messaggi di errore e usage in un CLI Unix-friendly?
fmt.Fprintln(???, "uso: prog ...")