Junedayday Blog

六月天天的个人博客

0%

【每周小结】2023-Week12

Go技巧 - 经典命令行工具库cobra

cobra简介

Go程序的主流交付的形式为2种:服务端程序与命令行工具。服务端程序一般长期运行在服务器上,而命令行工具则往往是一次性运行的,又被称为CLI,即client客户端的简写。

CLI工具的开发不难,但要做到方便易用,类似于Kuberneteskubectl,还是非常考验开发能力的。今天,我介绍一款开发命令行程序的利器 - cobra

cobra的安装命令如下:

1
go get -u github.com/spf13/cobra@latest

初始化

cobra init可以快速地初始化一个程序:

1
2
3
4
5
# 初始化 go module
go mod init github.com/Junedayday/cobra-demo

# 初始化cobra命令行工具,包括作者和License
cobra init --author "junedayday@gmail.com" --license apache

接下来,我会从结构与功能两个层面,对cobra进行介绍:

结构

新增子命令

1
2
3
# 新增子命令 create和delete
cobra add create
cobra add delete

在运行完上述两个命令后,我们编译一下程序,尝试着运行一下这个工具:

1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost] ./cobra-demo -h
Available Commands:
completion Generate the autocompletion script for the specified shell
create A brief description of your command
delete A brief description of your command
help Help about any command

[root@localhost] ./cobra-demo create
create called

[root@localhost] ./cobra-demo delete
delete called

相关的提示已经自动添加了。而整体的代码架构也很清晰:

1
2
3
4
5
- cmd/
- create.go
- delete.go
- root.go
- main.go

新增多级子命令

当工具变得复杂时,多级命令的可读性会比较棒,cobra也支持这个功能。假设我们要对上述的 cobra-demo create增加一种mock机制,即和正常创建方式区分开来,那么我们就可以运行以下命令:

1
2
# p即parent,对应的父命令
cobra add mock -p=createCmd

整个文件目录就成了

1
2
3
4
5
6
- cmd/
- create.go
- delete.go
- mock.go
- root.go
- main.go

重新编译后,命令行工具运行create时多了一个子命令提示:

1
2
3
[root@localhost] ./cobra-demo create
Available Commands:
mock A brief description of your command

功能

参数解析

cobra工具在生成时,支持对每一个命令进行参数解析。官方在生成代码时提供了示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var (
foo *string
toggle *bool
)

func init() {
rootCmd.AddCommand(createCmd)

// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
foo = createCmd.PersistentFlags().String("foo", "", "A help for foo")

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
toggle = createCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

这里的注释写得很清楚,参数分为2类:

  • Persistent Flags - 对该命令以及它的所有子命令生效
  • Local Flags - 仅对该命令生效

在复杂命令行工具的开发中,Flag经常会变得很多、很乱,我有两点建议:

  1. 用增加子命令的方式来减少入参的数量:参数越多,往往会让使用变得复杂性;而子命令相对而言更容易理解
  2. 特殊的功能单独开放一个子命令:隔离复杂度

Command钩子

cobra最核心的能力都放在结构体cobra.Command里,内部包含了诸多可自定义的能力,如简写、数据校验、版本等,而最核心的运行逻辑则是为以下5步(按顺序执行):

  • PersistentPreRun()
  • PreRun()
  • Run()
  • PostRun()
  • PersistentPostRun()

如果想要支持返回error,则将XXXRun()调整为XXXRunE()即可。cobra是一个开放的、普适性的工具,我们在使用它的特性时需要节制,才能保证后续的可维护性,例如:

  • 如果有复杂的业务功能,独立到另一个package中单独维护;
  • 如果有数据一致性的要求,就尽量维护在一个命令中操作,如Run

一般建议在cobra命令层做的只做三件事:提高命令的可读性解析参数校验参数的基本格式

小结

cobra工具是帮助开发者构建CLI程序的一款利器,核心优势在于能大幅度地提升使用者的体验(尤其是结合了Terminal中命令自动填充、美化等扩展能力)。

而在复杂项目中,则应提前考虑分层设计:隔离cobra生成代码与人工编写的业务代码 - 前者是命令式地执行,后者更需要抽象。

编程思考 - 以终为始的思维方式

以终为始,初听像是一个只停留于方法论的词汇,但它所代表的思维方式是非常有意义的。

以接口的两种风格为例:命令式 像是指挥者,必须很清楚内部的各项实现,然后指挥系统一步一步执行;而声明式则是告诉平台自己所期望的最终状态,而不关心其内部具体是怎么实现的。

命令式固然是一个很棒的解决方案,可以明确地指导我们达成目标,但过程中往往会遇到各种瓶颈,导致最终结果产生偏差;而声明式则是一种“以终为始”的解决方式,先明确自己的最终目标是什么,再逐步探索达到目标的路径。也许两者最终的效果一致,但“以终为始”的方式能让我们更聚焦于目标,减少过程中的损耗。

我们的开发工作也十分类似。过程中存在诸多的不确定性,未达预期是一种常态。以终为始的思路,能让我们减少复杂过程中的损耗。

工作生活 - 资产配置

最近我开始接触理财相关的概念,其中印象最为深刻的是 4321原则,即

  • 40%投资
  • 30%生活开销
  • 20%储蓄备用
  • 10%保险

各位如果和我一样是个理财新手,不妨先按这种方式梳理一下个人或家庭的资产情况。这个理论提供的只是参考值,真正的执行可以按需调整,重点是要有长期的投资思想,如个人成长、房地产、股票,在人生的不同阶段有不同选择。

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili: https://space.bilibili.com/293775192

公众号: golangcoding

二维码