
golang-cli
热门Golang CLI 应用程序开发。在构建、修改或审查 Go CLI 工具时使用——特别是命令结构、标志处理、配置分层、版本嵌入、退出码、I/O 模式、信号处理、Shell 补全、参数验证和 CLI 单元测试。当代码使用 cobra、viper 或 urfave/cli 时也会触发。对于 cobra 特定 API → 参见 `samber/cc-skills-golang@golang-spf13-cobra` 技能;对于 viper 配置分层 → 参见 `samber/cc-skills-golang@golang-spf13-viper` 技能。
Golang CLI 应用程序开发。在构建、修改或审查 Go CLI 工具时使用——特别是命令结构、标志处理、配置分层、版本嵌入、退出码、I/O 模式、信号处理、Shell 补全、参数验证和 CLI 单元测试。当代码使用 cobra、viper 或 urfave/cli 时也会触发。对于 cobra 特定 API → 参见 `samber/cc-skills-golang@golang-spf13-cobra` 技能;对于 viper 配置分层 → 参见 `samber/cc-skills-golang@golang-spf13-viper` 技能。
角色: 你是一名 Go CLI 工程师。你构建的工具应具有 Unix Shell 的原生感——可组合、可脚本化,并且在自动化下可预测。
模式:
- 构建 — 从头开始创建新的 CLI:按顺序遵循项目结构、根命令设置、标志绑定和版本嵌入部分。
- 扩展 — 向现有 CLI 添加子命令、标志或补全:首先读取当前命令树,然后应用与现有结构一致的更改。
- 审查 — 审计现有 CLI 的正确性:检查常见错误表,验证
SilenceUsage/SilenceErrors、标志到 Viper 的绑定、退出码以及 stdout/stderr 规范。
Go CLI 最佳实践
使用 Cobra + Viper 作为 Go CLI 应用程序的默认技术栈。Cobra 提供命令/子命令/标志结构,Viper 处理来自文件、环境变量和标志的配置,并自动分层。这种组合驱动了 kubectl、docker、gh、hugo 以及大多数生产级 Go CLI。
使用 Cobra 或 Viper 时,请参考库的官方文档和代码示例以获取当前 API 签名。
对于没有子命令且标志很少的简单单一用途工具,stdlib flag 就足够了。
快速参考
| 关注点 | 包 / 工具 |
|---|---|
| 命令和标志 | github.com/spf13/cobra |
| 配置 | github.com/spf13/viper |
| 标志解析 | github.com/spf13/pflag(通过 Cobra) |
| 彩色输出 | github.com/fatih/color |
| 表格输出 | github.com/olekukonko/tablewriter |
| 交互式提示 | github.com/charmbracelet/bubbletea |
| 版本注入 | go build -ldflags |
| 分发 | goreleaser |
项目结构
在 cmd/myapp/ 中组织 CLI 命令,每个命令一个文件。保持 main.go 最小化——它只调用 Execute()。
myapp/
├── cmd/
│ └── myapp/
│ ├── main.go # package main,只调用 Execute()
│ ├── root.go # 根命令 + Viper 初始化
│ ├── serve.go # "serve" 子命令
│ ├── migrate.go # "migrate" 子命令
│ └── version.go # "version" 子命令
├── go.mod
└── go.sum
main.go 应保持最小化——参见 assets/examples/main.go。
根命令设置
根命令通过 PersistentPreRunE 初始化 Viper 配置并设置全局行为。参见 assets/examples/root.go。
关键点:
SilenceUsage: true必须设置——防止在每个错误上打印完整的用法文本SilenceErrors: true必须设置——让你自己控制错误输出格式PersistentPreRunE在每个子命令之前运行,因此配置始终已初始化- 日志输出到 stderr,输出到 stdout
子命令
通过在 cmd/myapp/ 中创建单独的文件并在 init() 中注册它们来添加子命令。参见 assets/examples/serve.go 获取包含命令组的完整子命令示例。
标志
参见 assets/examples/flags.go 获取所有标志模式:
持久标志 vs 本地标志
- 持久 标志被所有子命令继承(例如
--config) - 本地 标志仅适用于定义它们的命令(例如
--port)
必需标志
使用 MarkFlagRequired、MarkFlagsMutuallyExclusive 和 MarkFlagsOneRequired 进行标志约束。
使用 RegisterFlagCompletionFunc 进行标志验证
为标志值提供补全建议。
始终将标志绑定到 Viper
这确保 viper.GetInt("port") 返回标志值、环境变量 MYAPP_PORT 或配置文件值——以优先级最高的为准。
参数验证
Cobra 为位置参数提供内置验证器。参见 assets/examples/args.go 获取内置和自定义验证示例。
| 验证器 | 描述 |
|---|---|
cobra.NoArgs |
如果提供了任何参数则失败 |
cobra.ExactArgs(n) |
需要恰好 n 个参数 |
cobra.MinimumNArgs(n) |
需要至少 n 个参数 |
cobra.MaximumNArgs(n) |
最多允许 n 个参数 |
cobra.RangeArgs(min, max) |
需要介于 min 和 max 之间 |
cobra.ExactValidArgs(n) |
恰好 n 个参数,必须在 ValidArgs 中 |
使用 Viper 进行配置
Viper 按以下顺序解析配置值(从高到低优先级):
- CLI 标志(显式用户输入)
- 环境变量(部署配置)
- 配置文件(持久设置)
- 默认值(在代码中设置)
参见 assets/examples/config.go 获取完整的 Viper 集成,包括结构体解组和配置文件监视。
示例配置文件 (.myapp.yaml)
port: 8080
host: localhost
log-level: info
database:
dsn: postgres://localhost:5432/myapp
max-conn: 25
使用上述设置,以下所有方式等效:
- 标志:
--port 9090 - 环境变量:
MYAPP_PORT=9090 - 配置文件:
port: 9090
版本和构建信息
版本应使用 ldflags 在编译时嵌入。参见 assets/examples/version.go 获取版本命令和构建说明。
退出码
退出码必须遵循 Unix 约定:
| 代码 | 含义 | 使用场景 |
|---|---|---|
| 0 | 成功 | 操作正常完成 |
| 1 | 一般错误 | 运行时失败 |
| 2 | 用法错误 | 无效的标志或参数 |
| 64-78 | BSD sysexits | 特定错误类别 |
| 126 | 无法执行 | 权限被拒绝 |
| 127 | 命令未找到 | 缺少依赖 |
| 128+N | 信号 N | 被信号终止(例如 130 = SIGINT) |
参见 assets/examples/exit_codes.go 获取将错误映射到退出码的模式。
I/O 模式
参见 assets/examples/output.go 获取所有 I/O 模式:
- stdout vs stderr:永远不要将诊断输出写入 stdout——stdout 用于程序输出(可管道化),stderr 用于日志/错误/诊断
- 检测管道 vs 终端:检查 stdout 上的
os.ModeCharDevice - 机器可读输出:支持
--output标志用于 table/json/plain 格式 - 颜色:使用
fatih/color,当输出不是终端时自动禁用
信号处理
信号处理必须使用 signal.NotifyContext 通过上下文传播取消。参见 assets/examples/signal.go 获取优雅的 HTTP 服务器关闭。
Shell 补全
Cobra 自动为 bash、zsh、fish 和 PowerShell 生成补全。参见 assets/examples/completion.go 获取补全命令和自定义标志/参数补全。
测试 CLI 命令
通过编程方式执行命令并捕获输出来测试命令。参见 assets/examples/cli_test.go。
在命令中使用 cmd.OutOrStdout() 和 cmd.ErrOrStderr()(而不是 os.Stdout / os.Stderr),以便输出可以在测试中被捕获。
常见错误
| 错误 | 修复 |
|---|---|
直接写入 os.Stdout |
测试无法捕获输出。使用 cmd.OutOrStdout(),测试可以将其重定向到缓冲区 |
在 RunE 内部调用 os.Exit() |
Cobra 的错误处理、延迟函数和清理代码永远不会运行。返回错误,让 main() 决定 |
| 未将标志绑定到 Viper | 标志无法通过环境变量/配置文件配置。为每个可配置标志调用 viper.BindPFlag |
缺少 viper.SetEnvPrefix |
PORT 与其他工具冲突。使用前缀(MYAPP_PORT)来命名空间环境变量 |
| 日志输出到 stdout | Unix 管道链式传递 stdout——日志会破坏下一个程序的数据流。日志应输出到 stderr |
| 每个错误都打印用法 | 每个错误都打印完整帮助文本是噪音。设置 SilenceUsage: true,将完整用法保留给 --help |
| 配置文件必需 | 没有配置文件的用户会崩溃。忽略 viper.ConfigFileNotFoundError——配置应该是可选的 |
未使用 PersistentPreRunE |
配置初始化必须在任何子命令之前发生。使用根命令的 PersistentPreRunE |
| 硬编码版本字符串 | 版本会与标签不同步。通过 ldflags 在构建时从 git 标签注入 |
不支持 --output 格式 |
脚本无法解析人类可读的输出。添加 JSON/table/plain 格式供机器消费 |
相关技能
参见 samber/cc-skills-golang@golang-project-layout、samber/cc-skills-golang@golang-dependency-injection、samber/cc-skills-golang@golang-testing、samber/cc-skills-golang@golang-design-patterns 技能。





