Lektionen des Moduls (4/5)
Miniprojekt: ein CLI mit Flags
Lassen Sie uns die Bausteine der vorherigen Module zusammenfügen, um eine kleine idiomatische CLI zu erstellen: Flag-Parsing mit dem flag-Paket, Behandlung von Positionsargumenten, Fehler bei os.Stderr und der richtige Exit-Code.
Das flag-Paket
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)
}
}Kernpunkte:
flag.Int,flag.String,flag.Bool,flag.Duration(usw.) geben einen Zeiger zurück: Sie verwenden*n,*upper.flag.Parse()muss EINMAL aufgerufen werden, vor dem Lesen der Werte und vorflag.Args().flag.Args()gibt die positionalen Argumente zurück (diejenigen nach--oder nach dem letzten Flag).- Akzeptierte Formen:
flag.String0,flag.String1,flag.String2. Boolesche Werte:flag.String3 (wahr),flag.String4.
Konventionelle Exit-Codes
os.Exit(0) // success
os.Exit(1) // generic error
os.Exit(2) // usage error (incorrect flag usage)Shells und Skripte erwarten diese Codes: Verwenden Sie sie konsequent. Niemals eine Nicht-Haupt-Goroutine mit os.Exit verlassen: Es wird jedes defer übersprungen, einschließlich der log-Leerung.
Stdderr vs Stdout
Alte und heilige Unix-Regel:
os.Stdout→ nützliche Ausgabe, weiterleitungsfähig (cli | grep ...).os.Stderr→ Diagnose, Fehler, Fortschrittsbalken, Nutzung.
fmt.Println("result: 42") // stdout
fmt.Fprintln(os.Stderr, "error: ...") // stderrAuf diese Weise zeigt cli 2>/dev/null nur die Ergebnisse an, cli >out.txt leitet nur das Ergebnis um und Fehler bleiben sichtbar.
log vs. fmt
fmtfür Benutzerausgabe.logfür die Diagnose: Fügt einen Zeitstempel hinzu, schreibt standardmäßig inos.Stderr, verfügt überlog.Fatal(Protokolle +os.Exit(1)) undlog.Panic.
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // prints and exits with 1Verwenden Sie für moderne Apps (Go 1.21+) log/slog für die strukturierte Protokollierung (JSON oder Text, Ebenen, typisierte Felder).
Architektur: main von der Logik trennen
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
}Vorteile:
- Testbar: Sie übergeben
bytes.Bufferals stdout/stderr und überprüfen die Ausgabe. - Kein globaler Status: Jeder
FlagSetist isoliert. mainbleibt winzig: Es bereitet IO + Delegierte vor.
Unterbefehle
Für CLIs mit Unterbefehlen (mycli serve, mycli migrate) ein Schema ohne externe Abhängigkeiten:
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)
}
}Jeder Unterbefehl hat seinen eigenen flag.NewFlagSet. Bei größeren Projekten strukturieren Bibliotheken wie cobra oder urfave/cli dieses Schema mit automatisch generierter Hilfe.
Übungen
Definieren Sie das Flag -n vom Typ int mit dem Wert von default 1 und beschreiben Sie es als 'Reparatur', dann verwenden Sie flag.Parse() und geben Sie den Wert ein.
Lösung nach 3 Versuchen verfügbar
Es ist nicht möglich, dass ein argomentales Argument für flag.Parse angezeigt wird, drücken Sie „Argument manuieren“ auf Stderr und beenden Sie es mit os.Exit(1).
Lösung nach 3 Versuchen verfügbar
Warum sendet der Stream Fehlermeldungen zu Fehlern und der Verwendung in einer Unix-freundlichen CLI?
fmt.Fprintln(???, "uso: prog ...")