关于 go-cli
面向 Go 开发者的命令行程序设计技能参考,适用于构建可测试、服务于用户和脚本的工具。内容涵盖标准流、参数、退出码、取消操作、测试、版本信息和 Shell 补全。
主要功能
- 01可测试的 run 函数结构
- 02stdout 与 stderr 约定
- 03标志、参数和退出码
- 04信号触发上下文取消
- 05面向人与机器的输出
- 06测试、版本信息与补全
适用场景
- 01设计可直接进行单元测试的 CLI
- 02将管道输出与诊断信息分开
- 03使命令响应 Ctrl+C 取消
- 04验证 CLI 行为和退出码
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-cli安装后在 Claude Code 中输入/go-cli确认已加载。
附带文件references/testing-and-versioning.md
详细介绍
Go 命令行设计
优秀的命令行工具应当像一个守规矩的 Unix 公民:优先使用标志,而不是魔法行为;数据输出到 stdout,诊断信息输出到 stderr;退出码应当让脚本可以信赖;按下 Ctrl+C 时,程序确实会停止。
详细参考资料,按需加载:
references/testing-and-versioning.md—main中的退出码映射、黄金文件和 testscript 测试、从构建信息读取版本、自动补全。
只有在下方摘要不够时,才阅读参考文件。
1. 结构:可测试的 main
func main() {
ctx, stop := signal.NotifyContext(context.Background(),
os.Interrupt, syscall.SIGTERM)
defer stop()
if err := run(ctx, os.Args[1:], os.Stdin, os.Stdout, os.Stderr); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
func run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
fs := flag.NewFlagSet("mytool", flag.ContinueOnError)
fs.SetOutput(stderr)
verbose := fs.Bool("v", false, "verbose output")
out := fs.String("o", "-", "output file (- for stdout)")
if err := fs.Parse(args); err != nil {
return err
}
// ...
_ = verbose
_ = out
return nil
}
signal.NotifyContext会让 Ctrl+C 取消上下文——每个耗时操作都接收ctx,并能干净地停止。上下文结束后调用一次stop(),这样再次按下 Ctrl+C 就能立即终止卡住的程序。run接收参数和流——测试可直接调用它,并使用strings.Reader/bytes.Buffer,无需启动子进程。os.Exit只在main中使用(它会跳过延迟调用)。
2. stdout 与 stderr
- stdout:程序的输出——数据、结果,也就是管道传输的内容。
- stderr:日志、进度、警告、用法错误。
--json或检测到管道(!term.IsTerminal(int(os.Stdout.Fd())))时,应禁用装饰效果,绝不能改变数据。
// ✅ Good — result to stdout, progress to stderr
fmt.Fprintf(stderr, "processed %d files\n", n)
fmt.Fprintln(stdout, result)
// ❌ Bad — mixing both into stdout breaks every pipe
fmt.Printf("processing...\ndone: %s\n", result)
3. 退出码
| Code | 含义 |
|---|---|
| 0 | 成功 |
| 1 | 通用运行时错误 |
| 2 | 用法错误(标志或参数错误)—— flag 包的约定 |
| >2 | 工具特有且有文档说明的含义(例如 grep 的 1 表示没有匹配项) |
在一个地方(main)将错误映射到退出码,不要在各处散布 os.Exit 调用。使用 flag.ContinueOnError 时,-h 会返回 flag.ErrHelp——对此应以 0 退出;并将解析错误包装为用法错误类型,以便 main 以 2 退出。按下 Ctrl+C 后以 130 退出。如果脚本需要根据不同失败类型采取不同操作,则定义哨兵错误并进行转换:errors.Is(err, ErrNoMatch) → 1。
4. 标志和参数
- 选项使用标志,主要操作对象使用位置参数:
mytool -v convert input.yaml,而不是mytool --input=input.yaml。 - 每个标志都应有用法说明;
-h/-help输出是主要的用户体验。 - 文件参数接受
-,表示“stdin/stdout”。 - 默认值必须安全:破坏性行为应由显式标志(
--force)启用,绝不能默认启用。 - 从环境变量或文件读取密钥,绝不要通过标志传入(
ps会泄露 argv)。
5. 子命令
标准库足以应对少量命令:
switch fs.Arg(0) {
case "serve":
return runServe(ctx, fs.Args()[1:], stdout, stderr)
case "migrate":
return runMigrate(ctx, fs.Args()[1:], stdout, stderr)
default:
fmt.Fprintln(stderr, usage)
return fmt.Errorf("unknown command %q", fs.Arg(0))
}
当需要嵌套命令、自动生成帮助和自动补全,以及大量标志时,采用 Cobra——它的结构足以抵消引入依赖的代价:
var rootCmd = &cobra.Command{Use: "mytool", SilenceUsage: true}
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the server",
RunE: func(cmd *cobra.Command, args []string) error {
return serve(cmd.Context(), addr) // RunE returns errors; no os.Exit
},
}
func init() {
serveCmd.Flags().StringVar(&addr, "addr", ":8080", "listen address")
rootCmd.AddCommand(serveCmd)
}
Cobra 使用规则:始终使用 RunE(绝不组合使用 Run 和 os.Exit),设置 SilenceUsage: true,避免运行时错误时输出帮助信息,并向下传递 cmd.Context()。只有在确实需要分层配置(标志 > 环境变量 > 文件)时才添加 Viper——对大多数工具来说,flag + os.Getenv 已经足够。
6. 面向用户和机器的输出
- 提供
--json标志供机器读取;默认使用表格或文本,面向用户。 - stdout 不是终端,或设置了
NO_COLOR时,绝不输出 ANSI 颜色。 - 进度条和加载动画输出到 stderr,且仅在终端中显示。
7. 测试
使用缓冲区直接测试 run,并将 stdout 与黄金文件进行比较;使用 -update 重新生成黄金文件。端到端行为——真实二进制、退出码、stdin——使用 testscript(.txtar 脚本)测试,这是 Go 命令自身测试所用的格式。
8. 版本信息
mytool version 通过 debug.ReadBuildInfo() 读取版本信息:从 Go 1.24 开始,主模块的版本来自 VCS 标签,vcs.revision 来自构建设置。只有在没有 VCS 元数据的情况下构建时,才回退到 -ldflags "-X main.version=..."。
9. Shell 自动补全
Cobra 可生成 completion bash|zsh|fish|powershell;添加 ValidArgsFunction 以支持动态值。不使用 Cobra 时,随发行版提供静态补全脚本。
第 3 节和第 7–9 节的示例见:references/testing-and-versioning.md。
验证清单
- 使用
run(ctx, args, stdin, stdout, stderr)模式——无需子进程即可测试逻辑 - 接入
signal.NotifyContext;长时间运行的操作应遵循 ctx 取消信号 - 数据写入 stdout,诊断信息写入 stderr——通过管道验证
- 退出码:成功为 0,用法错误为 2,其他情况使用文档说明的退出码;
os.Exit仅在 main 中使用 - 每个标志都有用法说明;检查过
-h的输出 - 接受
-作为读取 stdin 或写入 stdout 的文件参数 - 破坏性操作必须通过显式标志启用
- 不通过 argv 传递机密信息
- 若使用 Cobra:所有位置都使用 RunE,设置 SilenceUsage,并传递 context
- 在非 TTY 环境和 NO_COLOR 设置下禁用颜色和加载动画
-h以状态码 0 退出,用法错误以状态码 2 退出,Ctrl+C 以状态码 130 退出version输出来自构建信息;使用 golden 或 testscript 测试覆盖 CLI 的所有功能