Go 最适合交付的形态之一是单二进制 CLI。没有 JRE、没有 fat jar 解压目录——go build 出来拷走就能跑。这一节覆盖从标准库 flag 到 cobra,以及结构化日志 log/slog。
timeout := flag.Duration("timeout", 5*time.Second, "request timeout")
verbose := flag.Bool("v", false, "verbose")
flag.Parse()
args := flag.Args()子命令、嵌套配置、自动补全——flag 都不擅长。工具只有一两个全局选项时用它;再复杂就上 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 往往够了
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)。
- 业务逻辑进 library package,
main只解析参数——可测、可复用 - 错误用
return err,在main里统一os.Exit(1)——别在库里os.Exit/log.Fatal --dry-run对破坏性命令几乎总是值得加(项目 03 已经强调过)- 帮助文本写清楚例子——CLI 的文档第一页是
--help
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 的整套配置体系。
- 给一个命令增加
--json,让人类输出和机器输出共享同一份业务结果。 - 将一个在库函数中
log.Fatal的工具改为返回 error,并为失败路径写测试。 - 用
slog.NewTextHandler和slog.NewJSONHandler输出同一条事件,比较两者适用场景。
配套项目:11 脚手架生成器 · 12 Git changelog