שיעורי מודול (4/5)
מיני-פרויקט: CLI עם דגלים
בואו נחבר את אבני הבניין של המודולים הקודמים כדי לבנות CLI אידיומטי קטן: ניתוח דגל עם החבילה flag, טיפול בארגומנטים מיקוםיים, שגיאות ב-os.Stderr וקוד היציאה הנכון.
החבילה 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)
}
}נקודות מפתח:
flag.Int,flag.String,flag.Bool,flag.Duration(וכו') מחזירים מצביע: אתה משתמש ב-*n,*upper.- יש לקרוא ל-
flag.Parse()פעם אחת, לפני קריאת הערכים ולפניflag.Args(). flag.Args()מחזיר את הארגומנטים מיקום (אלה שאחרי--או אחרי הדגל האחרון).- טפסים מקובלים:
-n 3,-n=3,--n=3. בוליאניות:-u(נכון),-u=false.
קודי יציאה קונבנציונליים
os.Exit(0) // success
os.Exit(1) // generic error
os.Exit(2) // usage error (incorrect flag usage)צדפים וסקריפטים מצפים לקודים האלה: השתמש בהם באופן עקבי. לעולם לא תצא מגורוטינה שאינה ראשית עם os.Exit: הוא מדלג על כל defer, כולל שטיפה log.
סטדר נגד סטדאוט
כלל יוניקס ישן וקדוש:
os.Stdout→ פלט שימושי, ניתן לצינור (cli | grep ...).os.Stderr→ אבחון, שגיאות, פסי התקדמות, שימוש.
fmt.Println("result: 42") // stdout
fmt.Fprintln(os.Stderr, "error: ...") // stderrכך cli 2>/dev/null מציג רק את התוצאות, cli >out.txt מפנה רק את התוצאה והשגיאות נשארות גלויות.
log לעומת fmt
fmtעבור פלט משתמש.logלאבחון: מוסיף חותמת זמן, כותב ל-os.Stderrכברירת מחדל, ישlog.Fatal(יומנים +os.Exit(1)) ו-log.Panic.
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // prints and exits with 1עבור אפליקציות מודרניות (Go 1.21+) השתמש ב-log/slog לרישום מובנה (JSON או טקסט, רמות, שדות מוקלדים).
ארכיטקטורה: הפרד main מהלוגיקה
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
}יתרונות:
- ניתן לבדיקה: אתה מעביר את
bytes.Bufferבתור stdout/stderr ומאמת את הפלט. - אין מצב גלובלי: כל
FlagSetמבודדת. mainנשאר זעיר: הוא מכין IO + נציגים.
פקודות משנה
עבור CLIs עם פקודות משנה (mycli serve, mycli migrate), סכימה ללא תלות חיצונית:
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)
}
}לכל פקודה משנה יש flag.NewFlagSet משלה. עבור פרויקטים גדולים יותר, ספריות כמו cobra או urfave/cli בונות את הסכימה הזו בעזרת עזרה שנוצרה אוטומטית.
תרגילים
Definisci il flag -n di tipo int con valore di default 1 e descrizione 'ripetizioni', poi chiama flag.Parse() e stampa il valore.
הפתרון זמין לאחר 3 ניסיונות
Se non c'è almeno un argomento posizionale dopo flag.Parse, stampa 'manca argomento' su Stderr e termina con os.Exit(1).
הפתרון זמין לאחר 3 ניסיונות
האם ניתן להשתמש בהודעות שגיאה ב-CLI Unix ידידותי?
fmt.Fprintln(???, "uso: prog ...")