Lekcje modułu (4/5)
Mini-projekt: CLI z flagami
Połączmy elementy poprzednich modułów, aby zbudować mały idiomatyczny CLI: parsowanie flag za pomocą pakietu flag, obsługa argumentów pozycyjnych, błędy w os.Stderr i właściwy kod zakończenia.
Pakiet flag
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)
}
}Kluczowe punkty:
flag.Int,flag.String,flag.Bool,flag.Duration(itp.) zwracają wskaźnik: używasz*n,*upper.flag.Parse()należy wywołać RAZ, przed odczytaniem wartości i przedflag.Args().flag.Args()zwraca argumenty pozycyjne (te po--lub po ostatniej fladze).- Akceptowane formy:
flag.String0,flag.String1,flag.String2. Wartości logiczne:flag.String3 (true),flag.String4.
Konwencjonalne kody wyjścia
os.Exit(0) // success
os.Exit(1) // generic error
os.Exit(2) // usage error (incorrect flag usage)Powłoki i skrypty oczekują tych kodów: używaj ich konsekwentnie. Nigdy nie wychodź z innej niż główna gorrutyny za pomocą os.Exit: pomija każde defer, w tym opróżnianie log.
Stderr kontra standardowe wyjście
Stara i święta zasada Uniksa:
os.Stdout→ użyteczne wyjście, możliwość potokowania (cli | grep ...).os.Stderr→ diagnostyka, błędy, paski postępu, użytkowanie.
fmt.Println("result: 42") // stdout
fmt.Fprintln(os.Stderr, "error: ...") // stderrW ten sposób cli 2>/dev/null pokazuje tylko wyniki, cli >out.txt przekierowuje tylko wynik, a błędy pozostają widoczne.
KODEKF0 kontra KODEKF1
fmtdla danych wyjściowych użytkownika.logdo diagnostyki: dodaje znacznik czasu, domyślnie zapisuje doos.Stderr, malog.Fatal(logi +os.Exit(1)) ilog.Panic.
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // prints and exits with 1W przypadku nowoczesnych aplikacji (Go 1.21+) użyj log/slog do rejestrowania strukturalnego (JSON lub tekst, poziomy, pola wpisane).
Architektura: oddziel main od logiki
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
}Zalety:
- Testowalne: przekazujesz
bytes.Bufferjako stdout/stderr i sprawdzasz wyjście. - Brak stanu globalnego: każdy
FlagSetjest izolowany. mainpozostaje mały: przygotowuje delegatów IO +.
Podpolecenia
W przypadku interfejsów CLI z podkomendami (mycli serve, mycli migrate) schemat bez zewnętrznych zależności:
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)
}
}Każda podkomenda ma swój własny flag.NewFlagSet. W przypadku większych projektów biblioteki takie jak cobra lub urfave/cli tworzą ten schemat za pomocą automatycznie generowanej pomocy.
Ćwiczenia
Zdefiniuj flagę -n typ int z wartością domyślną 1 i opis „ripetizioni”, poi chiama flag.Parse() i stempel wartości.
Rozwiązanie dostępne po 3 próbach
Se non c'è almeno un argomento posizionale dopo flag.Parse, stampa 'manca argomento' su Stderr e enda con os.Exit(1).
Rozwiązanie dostępne po 3 próbach
Jak przesyłać strumieniowo komunikaty o błędach i używać ich w interfejsie CLI przyjaznym dla Uniksa?
fmt.Fprintln(???, "uso: prog ...")