模块 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 ...")