近期关于cobra库的一些实践心得总结
开源社区中,go 语言的 cli 库有很多,有名的也不少,个人觉得不需要都熟练,只需要针对一个用的熟就可以了,其他的稍作了解即可。
最近在实际开发工作中,针对 cobra 库又有了一些实践心得,这里做一个汇总记录,以后如果有新的内容,也会更新在这里。
# 打印版本
一个开源项目,以及二进制,自身附带详细的版本信息,还是非常重要的,这有助于用户了解自己当前所用的版本,也方便在问题反馈时,提供使用的版本号。
# 效果展示
在 cobra 中,版本信息可以通过自定义模板进行打印,我这里做出来的效果如下:
$ eryajfctl -v
🍉 eryajfctl version information:
Version: v0.1.0
Git Commit: a0ea03e
Go version: go1.24.2
OS/Arch: linux/amd64
Build Time: 2025-04-10 09:50:37
2
3
4
5
6
7
想要实现这个效果,需要两块儿地方协同定义。
# 代码定义
代码中的定义:
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "eryajfctl",
Short: "eryajf goscript",
Long: `🦄 利用cobra制作运维日常工具箱的框架。`,
Run: func(cmd *cobra.Command, args []string) {
// 检查是否有 -v 参数
if versionFlag, _ := cmd.Flags().GetBool("version"); versionFlag {
fmt.Println(cmd.VersionTemplate())
return
}
cmd.Help()
},
}
var (
Version string
GitCommit string
BuildTime string
)
func init() {
rootCmd.Version = Version
rootCmd.SetVersionTemplate(fmt.Sprintf(`🍉 {{with .Name}}{{printf "%%s version information: " .}}{{end}}
{{printf "Version: %%s" .Version}}
Git Commit: %s
Go version: %s
OS/Arch: %s/%s
Build Time: %s
`, GitCommit, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildTime))
rootCmd.Flags().BoolP("version", "v", false, "Show version information")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
简单做一下说明:
SetVersionTemplate
是 cobra 提供的一个定义版本模板的方法,可以调用并定义版本的格式及内容。这里的格式:反引号内定义的,和最终打印出来的是一样的,所以你要调整最终打印的效果,只需要调整反引号里边的排版即可。- 在运行命令处,默认情况下是打印帮助信息,在这个打印之前,需要先判断是否传递了
-v
参数,如果传递,则打印版本信息并退出。 - 版本号中打印的一些信息,取值于运行时的一些信息,这些信息,通过二进制构建时
-ldflags
传递。这块儿,我一般放到Makefile
中定义。
# 构建传递
通常,我会在 Makefile 中做如下定义:
# 定义项目名称
BINARY_NAME=eryajfctl
# 定义输出目录
OUTPUT_DIR=bin
VERSION = $(shell git describe --tags --always)
GIT_COMMIT = $(shell git rev-parse --short HEAD)
BUILD_TIME = $(shell date "+%F %T")
define LDFLAGS
"-X 'github.com/eryajf/eryajfctl/cmd.Version=${VERSION}' \
-X 'github.com/eryajf/eryajfctl/cmd.GitCommit=${GIT_COMMIT}' \
-X 'github.com/eryajf/eryajfctl/cmd.BuildTime=${BUILD_TIME}'"
endef
.PHONY: build
build:
go build -ldflags=${LDFLAGS} -o ${BINARY_NAME} main.go
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这里需要注意一点:定义 LDFLAGS 时,传递的 github.com/eryajf/eryajfctl/cmd
就是上边代码定义所处的包位置,这里一定要对应上,否则可能不会生效。
通常通过 make build
构建出来二进制,执行之后,就能看到对应的版本信息了。
# 环境变量
日常使用时,一般变量都是通过传参,或者配置文件定义,那么,是否有比较优雅的姿势,能够再兼容从环境变量中读取呢。
CNB (opens new window) 的插件 (opens new window)是以镜像的形式提供服务,并且所有的参数通过环境变量传递,我做了一些实践,社区提交的首个插件 doge-cdn-refresh (opens new window)。
实现思路大概如下:
func init() {
viper.AutomaticEnv()
viper.SetEnvPrefix("PLUGIN")
viper.BindEnv("ename") // 绑定环境变量 PLUGIN_ENAME
viper.BindEnv("epass") // 绑定环境变量 PLUGIN_EPASS
rootCmd.AddCommand(exCmd)
exCmd.AddCommand(ex.GetConfigCmd)
ex.GetConfigCmd.Flags().StringP("ename", "n", viper.GetString("ename"), "传入要打印的内容 [$PLUGIN_ENAME]")
viper.BindPFlag("ename", ex.GetConfigCmd.Flags().Lookup("ename"))
ex.GetConfigCmd.Flags().StringP("epass", "p", viper.GetString("epass"), "传入要打印的内容 [$PLUGIN_EPASS")
viper.BindPFlag("epass", ex.GetConfigCmd.Flags().Lookup("epass"))
// 遍历所有 flags,清除默认值占位符,避免在日志中打印
ex.GetConfigCmd.Flags().VisitAll(func(f *pflag.Flag) {
f.DefValue = ""
})
ex.GetConfigCmd.PreRunE = func(cmd *cobra.Command, args []string) error {
if viper.GetString("ename") == "" {
return fmt.Errorf("必须通过环境变量 PLUGIN_ENAME 或命令行参数 --ename 提供值")
}
if viper.GetString("epass") == "" {
return fmt.Errorf("必须通过环境变量 PLUGIN_EPASS 或命令行参数 --epass 提供值")
}
return nil
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
这里有几个注意点:
- 首先利用 viper 读取环境变量,然后把读取的值放到参数的默认值中。
- 同时再通过
viper.BindPFlag
将命令行参数的值赋值给 viper,这么做的目的,是为了便于做下边的校验,也就是用户无论是通过环境变量,还是通过参数,必须传递 ename 才能正常执行ex.GetConfigCmd
的逻辑,否则就提示用户输出该参数。 - 还有非常重要的一项,是把所有参数的默认值的输出,给去掉,这个细节很重要,不能因为 a 参数没有传递,就把 b 参数传递的值给暴漏了,如下是对比。第一次执行是未做默认值隐藏,会把密码打印到终端,第二次执行则把默认值隐藏掉了,就不会打印了。
这里为什么使用viper获取环境变量而不是直接使用os.Getenv()
呢?
- 首先viper是cobra官方提供的,功能更强大,和cobra的融合也非常好,使用很方便。
- 很重要的一点,vipre在获取环境变量的时候,还支持识别变量的类型,从而直接传递给cobra使用。上边例子中都是string类型,比如下边还可以改成[]string类型:
// 首先从环境变量中获取配置
viper.AutomaticEnv()
viper.SetEnvPrefix("PLUGIN")
viper.BindEnv("urls")
rootCmd.Flags().StringSliceP("urls", "u", strings.Split(viper.GetString("urls"), ","), "Refresh URLs [$PLUGIN_URLS]")
2
3
4
5
# 生成文档
cobra 提供了自动生成命令行工具文档的能力,只需添加如下代码定义:
func init() {
rootCmd.Flags().BoolVarP(&MarkdownDocs, "md-docs", "m", false, "gen Markdown docs")
}
var MarkdownDocs bool
func GenDocs() {
if MarkdownDocs {
if err := doc.GenMarkdownTree(rootCmd, "./docs"); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
然后把 GenDocs()
放到 main 函数中,通过执行:go run main.go -m
就会自动在 docs
目录下生成这个命令行工具的使用指南,包括每个参数的方法定义。
文档还自动生成了不同参数的独立文档,可通过超链接跳转查看。
以上,是目前个人在实际开发过程中遇到的一些功能点的实践心得,事实上我还想增加一个自动生成配置文件的能力,目前需求不强,还没有深入探索,如果有这块儿实践心得的同学,欢迎评论区交流。


- 01
- 学习周刊-总第208期-2025年第17周04-24
- 02
- Go开发实践之Gin框架将前端的dist目录embed到二进制04-22
- 03
- 学习周刊-总第207期-2025年第16周04-17