![Go语言编程之旅:一起用Go做项目](https://wfqqreader-1252317822.image.myqcloud.com/cover/485/32441485/b_32441485.jpg)
第1章 命令行应用:打造属于自己的工具集
1.1 工具之旅
绝大部分工程师都想拥有一个属于自己的工具集,因为它能够在提高工作效率的同时,给我们带来一定的成就感。更重要的是,在持续不断地维护、迭代项目的同时,我们的技术也会得到磨炼,因为我们遇到的问题,极有可能是共性问题,也就是说,别人可能也会遇到。事实上,GitHub里的许许多多的优秀个人开源项目就是这样产生的,因而开源工具集是一件一举多得的事情。
在本章中,我们将做一个简单的通用工具集,用它解决在平时工作中经常遇到的一些小麻烦,而不再借助其他快捷网站,即让我们自己的产品为自己服务,并不断地迭代它。
1.1.1 标准库flag
标准库flag是Go语言中的一大利器,它的主要功能是实现命令行参数的解析,让我们在开发过程中能够非常方便地解析和处理命令行参数,是一个需要必知必会的基础标准库。因此,在本章中,我们对标准库flag进行基本的讲解。
在项目的后续中,我们使用开源项目Cobra快速构建CLI应用程序。Cobra非常的便捷和强大,目前市面上许多著名的Go语言开源项目都是使用Cobra构建的,如Kubernetes、Hugo、etcd、Docker等。Cobra是一个非常可靠的开源项目。
1.1.2 初始化项目
首先创建本项目的项目目录(本书介绍的创建命令均为类UNIX系统下的访问路径,若为Windows系统,则可根据实际情况自行调整项目路径),然后执行如下命令:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_19_1.jpg?sign=1740121009-uaJubXkt6QRPv6MhZSnF1xVG3Kfl5lHM-0-78135f2e53bce58b19504b630778c5d0)
在执行命令后,我们就已经完成了初始化项目的第一步,各命令的含义如下:
● 确定本书的项目工作路径,并循环递归创建tour项目目录。
● 切换当前工作区到tour项目目录下。
● 初始化项目的Go modules,设置项目的模块路径。
需要注意的是,我们在依赖管理上使用的是Go modules(详细介绍参见附录A),即系统环境变量GO111MODULE为auto或on(开启状态)。若在初始化Go modules时出现相关错误提示,则应当开启Go modules,命令如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_20_1.jpg?sign=1740121009-g8UB4q8VsgCOiuz9kjBno8dc1wtL4VAP-0-7f78b522ced08645eec96d45932487fa)
在执行这条命令后,Go工具集会将系统环境变量GO111MODULE设置为on。需要注意的是,因为语句go env-w并不支持覆写,所以可手动设置export GO111MODULE=on。
另外,若是初次使用Go modules,则建议设置国内镜像代理,否则会出现外网模块“拉”不下来的问题,设置命令如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_20_2.jpg?sign=1740121009-9ioBJzHsX7rErGKPfpmh98EFo10hpW7p-0-2896618d14de18d5ca021876d378b465)
1.1.3 示例
1.标准库flag的基本使用和长短选项
下面编写一个简单的示例,帮助我们了解标准库flag的基本使用,代码如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_20_3.jpg?sign=1740121009-fBlLxnSLD0nIoAmJYu6rl5QP4YCqos70-0-ceeebf54b52de54dc7e25a4608a31a82)
上述代码可以调用标准库flag的StringVar方法实现对命令行参数name的解析和绑定,其各个形参的含义分别为命令行标识位的名称、默认值和帮助信息。命令行参数支持如下三种命令行标志语法:
●-flag:仅支持布尔类型。
●-flag x:仅支持非布尔类型。
●-flag=x:都支持。
同时,标准库flag还提供了多种类型参数的绑定方式,读者根据各自应用程序的使用情况选用即可
运行该程序,检查输出结果与预想的是否一致,命令如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_21_1.jpg?sign=1740121009-RolgWYyzPpVOzd2GxyRtzOiAx4z8p4Ju-0-2c9f51abf3381fce10f5c6261f74fa8b)
由此可以发现,输出的结果是最后一个赋值的变量,也就是-n。
为什么长短选项要分为两次调用?一个命令行参数的标志位有长短选项是常规需求,而分开调用岂不是逻辑重复,有没有优化的方法呢?
实际上,标准库flag并不直接支持该功能,但是我们可以通过其他第三方库来实现这个功能,具体实现方法在本书后面介绍。
2.子命令的使用
在日常使用的CLI应用程序中,最常见的功能是子命令的使用。一个工具可能包含了大量相关联的功能命令,以此形成工具集,可以说是刚需,那么这个功能在标准库flag中是如何实现的呢?示例如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_21_2.jpg?sign=1740121009-Qb2APiLgpHcynGRRur2iDLbdY3bPeewF-0-dc5fbdea4c9cead5eab7b0f439e49339)
在上述代码中,我们首先调用了flag.Parse方法,将命令行解析为定义的标志,以便后续的参数使用。
另外,由于需要处理子命令,因此调用了flag.NewFlagSet方法。该方法会返回带有指定名称和错误处理属性的空命令集,相当于创建一个新的命令集去支持子命令。
需要注意的是,flag.NewFlagSet方法的第二个参数是ErrorHandling,用于指定处理异常错误,其内置了以下三种模式:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_22_1.jpg?sign=1740121009-Bea6GqGBb4pgtqzN4QeONI9kMrHTppOa-0-991368b4af06b07fd8eaf248f9158ed6)
接下来运行针对子命令的示例程序,对正常场景和异常场景进行检查,代码如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_22_2.jpg?sign=1740121009-SHGN4gp6nlpcLIItmOKRUsgn5yigvZe5-0-73fd115b72f8b2343e7a257100accba9)
通过输出结果可以看出,这段示例程序已经准确地识别了不同的子命令,并且因为ErrorHandling传递的是ExitOnError级别的命令,因此当识别出传递的命令行参数标志是未定义的时,会直接退出程序并提示错误信息。
1.1.4 分析
从使用上来讲,标准库flag非常方便,一个简单的CLI应用很快就搭建起来了,但是它是如何实现的呢?我们一起来深入看看,做到知其然并知其所以然。整体分析流程如图1-1所示。
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_23_1.jpg?sign=1740121009-sOPfc6hLwUEWTC1gkK9AsVyP5cSLz68U-0-1f08d0a615ecaea4caf3b049da6c8609)
图1-1
1.flag.Parse
在图1-1中,首先看到的是flag.Parse(简称Parse方法)。它总是在所有命令行参数注册的最后进行调用,其功能是解析并绑定命令行参数。下面一起看看其内部实现:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_23_2.jpg?sign=1740121009-AF4dhnLHMQ6uCgbKsffG9oxpTJliuG0H-0-8bcbf706aa3a07ed8fdff9a5cc1ae49c)
Parse方法调用NewFlagSet方法实例化了一个新的空命令集,然后通过调用os.Args把新的空命令集作为外部参数传入。
需要注意的是,Parse 方法使用的是 CommandLine 变量,它默认传入的 ErrorHandling 是ExitOnError。也就是说,如果在解析时遇到异常或错误,就会直接退出程序。如果不希望只要应用程序解析命令行参数失败,就导致应用启动中断,则需要进行额外的处理。
2.FlagSet.Parse
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_23_3.jpg?sign=1740121009-phxfShlzoIG0BVXh7AoJYnswrXumblil-0-c9a503385f4e3fe8d90c67727c5b0696)
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_24_1.jpg?sign=1740121009-zSJwXOQfCkeHiPnSRnWFKLNVK3LZglQF-0-9f71bc66e83e734f0059c14947496092)
FlagSet.Parse是对解析方法的进一步封装,实际上解析逻辑放在了parseOne中,而解析过程中遇到的一些特殊情况,如重复解析、异常处理等,均直接由FlagSet.Parse进行处理。实际上,这是一个分层明显、结构清晰的方法设计,值得我们参考。
3.FlagSet.parseOne
FlagSet.parseOne 是命令行解析的核心方法,所有的命令最后都会流转到 FlagSet.parseOne中进行处理,代码如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_24_2.jpg?sign=1740121009-EfQAAPREV8I44MXBjxa2Su68UXvvI37L-0-0a761598154add744e097fd8d8205476)
在上述代码中,主要是针对一些不符合命令行参数绑定规则的校验进行处理,大致分为以下四种情况:
● 命令行参数长度为0。
● 长度小于2或不满足flag标识符“-”。
● 如果flag标志位为“--”,则中断处理,并跳过该字符,也就是后续会以“-”进行处理。
● 在处理flag标志位后,如果取到的参数名不符合规则,则也将中断处理。例如,如果出现go run main.go go---name=eddycjy,就会返回错误提示bad flag syntax。
在定位命令行参数节点上,采用的依据是根据“-”的索引定位解析出上下参数的名(name)和参数的值(value),部分核心代码如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_25_1.jpg?sign=1740121009-5oq9a3E6cYDg3DhoFJTP5wAh096JH3xi-0-1b5554c09620f7fcc644a46b771bc472)
}在设置参数值时,会对值类型进行判断。若是布尔类型,则调用定制的boolFlag类型进行判断和处理。最后,通过该flag提供的Value.Set方法将参数值设置到对应的flag中,核心代码如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_25_2.jpg?sign=1740121009-ZVEaGl5eB1tKKt9581e4qrp1o21qzHUa-0-af808e48f17f1324183001ca104ea08d)
1.1.5 定义参数类型
在前面的分析中,flag的命令行参数类型是可以自定义的。也就是说,在Value.Set方法中,我们只需实现其对应的Value相关的两个接口就可以了,代码如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_26_1.jpg?sign=1740121009-dbNxu8fKtQJS0MRy6E9XBncLGpJYiD6M-0-51f5b1d5564c85317a39dfae16af8e32)
我们将原先的字符串变量name修改为类别别名,并为其定义符合Value的两个结构体方法,代码如下:
![](https://epubservercos.yuewen.com/4AF54D/17518673407513106/epubprivate/OEBPS/Images/39074_26_2.jpg?sign=1740121009-yFa5sr7X6LmzinhoiFgl1RIP6vW11vTl-0-6efbfa0fc5db490f07d92b4cac616b23)
该示例的最终输出结果为 name:eddycjy:Go 编程之旅,也就是说,只要我们实现了 Value的 String 方法和 Set方法,就可以进行定制化,然后无缝地接入我们的命令行参数的解析中,这就是Go语言的接口设计魅力之处。
1.1.6 小结
本节我们对最常用的标准库flag进行了简要的介绍。标准库flag的使用将始终穿插在本书的所有章节中,因为我们需要常常读取外部命令行的参数,例如启动端口号、日志路径设置等。