Chuyển đến nội dung chính
Mô-đun 10 · Bài học 4 trong tổng số 549/50 trong khóa học~18 min
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

Go
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ước flag.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

Go
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.
Go
fmt.Println("result: 42")                   // stdout
fmt.Fprintln(os.Stderr, "error: ...")       // stderr

Bằ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

  • fmt cho đầu ra của người dùng.
  • log để chẩn đoán: thêm dấu thời gian, ghi vào os.Stderr theo mặc định, có log.Fatal (logs + os.Exit(1)) và log.Panic.
Go
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

Go
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.Buffer dướ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 FlagSet bị cô lập.
  • main vẫ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:

Go
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

tập thể dục#go.m10.l4.e1
Nỗ lực: 0Đang tải…

Đị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.

Đang tải trình chỉnh sửa…

Giải pháp khả dụng sau 3 lần thử

tập thể dục#go.m10.l4.e2
Nỗ lực: 0Đang tải…

Se non c'è almeno un argomento posizionale dopo flag.Parse, stampa 'manca argomento' su Stderr e endina con os.Exit(1).

Đang tải trình chỉnh sửa…

Giải pháp khả dụng sau 3 lần thử

Quiz#go.m10.l4.e3
Sẵn sàng

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?

Go
fmt.Fprintln(???, "uso: prog ...")
Tùy chọn trả lời