Skip to content

Latest commit

 

History

History
136 lines (97 loc) · 4.8 KB

02-cli-and-slog.md

File metadata and controls

136 lines (97 loc) · 4.8 KB

CLI 与 slog

Go 最适合交付的形态之一是单二进制 CLI。没有 JRE、没有 fat jar 解压目录——go build 出来拷走就能跑。这一节覆盖从标准库 flag 到 cobra,以及结构化日志 log/slog。

flag:够用就别上框架

timeout := flag.Duration("timeout", 5*time.Second, "request timeout")
verbose := flag.Bool("v", false, "verbose")
flag.Parse()
args := flag.Args()

子命令、嵌套配置、自动补全——flag 都不擅长。工具只有一两个全局选项时用它;再复杂就上 cobra。

cobra:子命令的事实标准

root := &cobra.Command{Use: "scaffold", Short: "Generate a Go project"}
newCmd := &cobra.Command{
    Use:   "new [name]",
    Short: "Create a new project",
    Args:  cobra.ExactArgs(1),
    RunE: func(cmd *cobra.Command, args []string) error {
        return generate(args[0])
    },
}
root.AddCommand(newCmd)
root.Execute()

对照 picocli / Spring Shell:cobra 用代码组装命令树,而不是注解扫描。测试时可以直接调 cmd.Execute() 或把业务逻辑抽成库函数(推荐后者——项目 11 把 Generate 做成可单测的 API,cobra 只做薄壳)。

常见搭配:

  • cobra:命令行解析、帮助、子命令
  • viper:配置分层(flag / env / 文件)。小工具可以不用,环境变量 + flag 往往够了

slog:标准库结构化日志

Go 1.21 起 log/slog 进入标准库:

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
}))
logger.Info("server started", "addr", addr, "env", env)

对比 SLF4J/Logback:

点 Java Go slog
API log.info("x={}", x) logger.Info("x", "x", x) 键值对
后端 Logback/Log4j 配置 XML Handler 接口,JSON/Text 内建
上下文 MDC / ThreadLocal 把 logger 存进 context,或 With 出子 logger
多路输出 Appender 组合 slog.Handler 包装;Go 1.26+ 有 NewMultiHandler

请求级字段的惯用写法:

reqLog := logger.With("request_id", id, "user", user)
reqLog.Info("handled", "path", r.URL.Path, "ms", elapsed.Milliseconds())

不要拿 slog 当 fmt.Println 的替代品刷调试信息——CLI 的人类可读输出用 fmt,机器可读事件用 slog。

单二进制交付

CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o scaffold ./cmd/scaffold
点 Java Go
分发 jlink / fat jar + JRE 一个可执行文件
交叉编译 困难(尤其 native) GOOS=linux GOARCH=amd64 go build
启动 JVM 热身 毫秒级

这也是云原生里 Go 占优的原因之一:镜像可以小到几 MB(见 Part 6)。

设计建议

  1. 业务逻辑进 library package,main 只解析参数——可测、可复用
  2. 错误用 return err,在 main 里统一 os.Exit(1)——别在库里 os.Exit / log.Fatal
  3. --dry-run 对破坏性命令几乎总是值得加(项目 03 已经强调过)
  4. 帮助文本写清楚例子——CLI 的文档第一页是 --help

stdout、stderr 与退出码

CLI 是被人和脚本共同调用的 API。建议约定:

  • 正常结果写 stdout,便于通过管道传给下一个命令。
  • 警告和错误写 stderr。
  • 用法错误、业务失败和内部错误至少都应返回非零状态。
  • 库包不调用 os.Exit,只返回 error。
func run(args []string, stdout, stderr io.Writer) error {
    // parse, validate, execute
    return nil
}

func main() {
    if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

对大型 CLI,这种 run 边界比在测试里启动子进程更轻。

配置优先级

工具需要 flag、环境变量和配置文件时,要明确覆盖顺序,例如:

flag > environment > config file > default

只有在读取嵌套配置、多种文件格式或动态刷新时,Viper 的成本才往往值得。一个只有两个参数的 CLI 不需要复制 Spring Boot 的整套配置体系。

练习

配套材料:题目骨架 · 参考答案

  1. 给一个命令增加 --json,让人类输出和机器输出共享同一份业务结果。
  2. 将一个在库函数中 log.Fatal 的工具改为返回 error,并为失败路径写测试。
  3. 用 slog.NewTextHandler 和 slog.NewJSONHandler 输出同一条事件,比较两者适用场景。

上一节:标准库核心 | 下一节:测试体系

配套项目:11 脚手架生成器 · 12 Git changelog