Bài học theo mô-đun (4/5)
Dự án nhỏ: CLI có cờ
Chúng ta hãy tập hợp các khối xây dựng của các mô-đun trước để xây dựng một CLI thành ngữ nhỏ: phân tích cú pháp cờ với gói flag, xử lý các đối số vị trí, lỗi trên os.Stderr và mã thoát bên phải.
Gói 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)
}
}Những điểm chính:
flag.Int,flag.String,flag.Bool,flag.Duration(v.v.) trả về con trỏ: bạn sử dụng*n,*upper.flag.Parse()phải được gọi MỘT LẦN, trước khi đọc các giá trị và trướcflag.Args().flag.Args()trả về các đối số vị trí (các đối số sau--hoặc sau cờ cuối cùng).- Các biểu mẫu được chấp nhận:
-n 3,-n=3,--n=3. Giá trị Boole:-u(đúng),-u=false.
Mã thoát thông thường
os.Exit(0) // success
os.Exit(1) // generic error
os.Exit(2) // usage error (incorrect flag usage)Shell và script yêu cầu các mã này: hãy sử dụng chúng một cách nhất quán. Không bao giờ thoát khỏi một goroutine không chính bằng os.Exit: nó bỏ qua mọi defer, bao gồm cả log lần xả.
Stderr vs Stdout
Quy tắc Unix cũ và thiêng liêng:
os.Stdout→ đầu ra hữu ích, có thể chuyển đổi thành đường ống (cli | grep ...).os.Stderr→ chẩn đoán, lỗi, thanh tiến trình, cách sử dụng.
fmt.Println("result: 42") // stdout
fmt.Fprintln(os.Stderr, "error: ...") // stderrBằng cách này cli 2>/dev/null chỉ hiển thị kết quả, cli >out.txt chỉ chuyển hướng kết quả và lỗi vẫn hiển thị.
log đấu với fmt
fmtcho đầu ra của người dùng.logđể chẩn đoán: thêm dấu thời gian, ghi vàoos.Stderrtheo mặc định, cólog.Fatal(logs +os.Exit(1)) vàlog.Panic.
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // prints and exits with 1Đối với các ứng dụng hiện đại (Go 1.21+), hãy sử dụng log/slog để ghi nhật ký có cấu trúc (JSON hoặc văn bản, cấp độ, trường đã nhập).
Kiến trúc: tách biệt main khỏi 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
}Thuận lợi:
- Có thể kiểm tra: bạn chuyển
bytes.Bufferdưới dạng thiết bị xuất chuẩn/thiết bị xuất chuẩn và xác minh kết quả đầu ra. - Không có trạng thái chung: mỗi
FlagSetbị cô lập. mainvẫn nhỏ bé: nó chuẩn bị các đại biểu IO +.
Lệnh phụ
Đối với CLI có lệnh phụ (mycli serve, mycli migrate), lược đồ không có phần phụ thuộc bên ngoài:
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)
}
}Mỗi tiểu ban có flag.NewFlagSet riêng. Đối với các dự án lớn hơn, các thư viện như cobra hoặc urfave/cli cấu trúc lược đồ này với trợ giúp được tạo tự động.
Bài tập
Định nghĩa cờ -n di tipo int con value mặc định 1 và mô tả 'ripetizioni', poi chiama flag.Parse() và stampa il valore.
Giải pháp khả dụng sau 3 lần thử
Se non c'è almeno un argomento posizionale dopo flag.Parse, stampa 'manca argomento' su Stderr e endina con os.Exit(1).
Giải pháp khả dụng sau 3 lần thử
Bạn có thể gặp lỗi khi sử dụng luồng thông báo lỗi trong CLI Unix-thân thiện?
fmt.Fprintln(???, "uso: prog ...")