跳转到主要内容
模块 10 · 第 4 课(共 5)课程中的49/50~18 min
模块课程(4/5)

迷你项目:带有标志的 CLI

让我们将前面模块的构建块放在一起,构建一个小型的惯用 CLI:使用 flag 包进行标志解析、处理位置参数、os.Stderr 上的错误以及正确的退出代码。

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)
    }
}

要点:

  • 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。

常规退出代码

Go
os.Exit(0) // success
os.Exit(1) // generic error
os.Exit(2) // usage error (incorrect flag usage)

Shell 和脚本需要这些代码:一致地使用它们。 永远不要使用 os.Exit 从非主 goroutine 退出:它会跳过每个 defer,包括 log 刷新。

标准错误与标准输出

古老而神圣的 Unix 规则:

  • os.Stdout → 有用的输出,可通过管道传输 (cli | grep ...)。
  • os.Stderr → 诊断、错误、进度条、用法。
Go
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。
Go
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // prints and exits with 1

对于现代应用程序 (Go 1.21+),请使用 log/slog 进行结构化日志记录(JSON 或文本、级别、类型字段)。

架构:将 main 与逻辑分开

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
}

优点:

  • 可测试:您将 bytes.Buffer 作为 stdout/stderr 传递并验证输出。
  • 无全局状态:每个 FlagSet 都是隔离的。
  • main 保持很小:它准备 IO + 委托。

子命令

对于带有子命令(mycli serve、mycli migrate)的 CLI,无外部依赖项的方案:

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)
    }
}

每个子命令都有自己的 flag.NewFlagSet。对于较大的项目,像 cobra 或 urfave/cli 这样的库会使用自动生成的帮助来构造此方案。

练习

锻炼#go.m10.l4.e1
尝试:0加载中...

定义标志 -n 为默认值 1 的类型并描述“ripetizioni”,poi chiama flag.Parse() 和标志值。

正在加载编辑器...

3 次尝试后可用的解决方案

锻炼#go.m10.l4.e2
尝试:0加载中...

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

正在加载编辑器...

3 次尝试后可用的解决方案

Quiz#go.m10.l4.e3
准备好

在 un CLI Unix 友好中是否可以使用流标记消息错误?

Go
fmt.Fprintln(???, "uso: prog ...")
答案选项