c05_03.md 8.4 KB
Newer Older
王炳明 已提交
1 2 3 4
# 5.3 命令行参数的解析:flag 库详解

![](http://image.iswbm.com/20200607145423.png)

写代码的明哥's avatar
update  
写代码的明哥 已提交
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 34 35 36 37 38 39 40 41 42 43 44
在 Golang 程序中有很多种方法来处理命令行参数。

简单的情况下可以不使用任何库,直接使用 `os.Args`

```go
package main

import (
	"fmt"
	"os"
)

func main() {
	//os.Args是一个[]string
	if len(os.Args) > 0 {
		for index, arg := range os.Args {
			fmt.Printf("args[%d]=%v\n", index, arg)
		}
	}
}
```

试着运行一下,第一个参数是执行文件的路径。

```shell
$ go run demo.go hello world hello golang
args[0]=/var/folders/72/lkr7ltfd27lcf36d75jdyjr40000gp/T/go-build187785213/b001/exe/demo
args[1]=hello
args[2]=world
args[3]=hello
args[4]=golang
```

从上面你可以看到,`os.Args` 只能处理简单的参数,而且对于参数的位置有严格的要求。对于一些比较复杂的场景,就需要你自己定义解析规则,非常麻烦。

如果真的遇上了所谓的复杂场景,那么还可以使用 Golang 的标准库 flag 包来处理命令行参数。

本文将介绍 Golang 标准库中 flag 包的用法。

## 1. 参数种类
王炳明 已提交
45 46 47 48 49 50 51 52 53 54 55

根据参数是否为布尔型,可以分为两种:

- 布尔型参数:如 `--debug`,后面不用再接具体的值,指定就为 True,不指定就为 False非布尔型参数
- 非布尔型参数:非布尔型,有可能是int,string 等其他类型,如 `--name jack` ,后面可以接具体的参数值

根据参数名的长短,还可以分为:

- 长参数:比如 `--name jack` 就是一个长参数,参数名前有两个 `-`
- 短参数:通常为一个或两个字母(是对应长参数的简写),比如 `-n` ,参数名前只有一个 `-`

写代码的明哥's avatar
update  
写代码的明哥 已提交
56
## 2. 入门示例
王炳明 已提交
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86

我先用一个字符串类型的参数的示例,抛砖引玉

```go
package main

import (
	"flag"
	"fmt"
)

func main(){
    var name string
    flag.StringVar(&name, "name", "jack", "your name")
    
flag.Parse()  // 解析参数
	fmt.Println(name)
}
```

`flag.StringVar` 定义了一个字符串参数,它接收几个参数

- 第一个参数 :接收值后,存放在哪个变量里,需为指针
- 第二个参数 :在命令行中使用的参数名,比如 `--name jack` 里的 name 
- 第三个参数 :若命令行中未指定该参数值,那么默认值为 `jack`
- 第四个参数:记录这个参数的用途或意义

运行以上程序,输出如下

```go
写代码的明哥's avatar
update  
写代码的明哥 已提交
87 88 89
$ go run demo.go
jack

王炳明 已提交
90 91 92 93
$ go run demo.go --name wangbm
wangbm
```

写代码的明哥's avatar
update  
写代码的明哥 已提交
94
## 3. 改进一下
王炳明 已提交
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121

如果你的程序只接收很少的几个参数时,上面那样写也没有什么问题。

但一旦参数数量多了以后,一大堆参数解析的代码堆积在 main 函数里,影响代码的可读性、美观性。

建议将参数解析的代码放入 `init` 函数中,`init` 函数会先于 `main` 函数执行。

```go
package main

import (
	"flag"
	"fmt"
)

var name string

func init()  {
	flag.StringVar(&name, "name", "jack", "your name")
}

func main(){
	flag.Parse()
	fmt.Println(name)
}
```

写代码的明哥's avatar
update  
写代码的明哥 已提交
122
## 4. 参数类型
王炳明 已提交
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151

当你在命令行中指定了参数,Go 如何解析这个参数,转化成何种类型,是需要你事先定义的。

不同的参数,对应着 `flag` 中不同的方法。

下面分别讲讲不同的参数类型,都该如何定义。

### 布尔型

**实现效果**:当不指定 `--debug` 时,debug 的默认值为 false,你一指定 `--debug`,debug 为赋值为 true。

```go
var debug bool

func init()  {
	flag.BoolVar(&debug, "debug", false, "是否开启 DEBUG 模式")
}

func main(){
	flag.Parse()
	fmt.Println(debug)
}
```

运行后,执行结果如下

```shell
$ go run main.go 
false
写代码的明哥's avatar
update  
写代码的明哥 已提交
152

王炳明 已提交
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
$ go run main.go --debug
true
```

### 数值型

定义一个 age 参数,不指定默认为 18 

```go
var age int

func init()  {
	flag.IntVar(&age, "age", 18, "你的年龄")
}

func main(){
	flag.Parse()
	fmt.Println(age)
}
```

运行后,执行结果如下

```shell
$ go run main.go 
18
写代码的明哥's avatar
update  
写代码的明哥 已提交
179

王炳明 已提交
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
$ go run main.go --age 20
20
```

`int64``uint``float64` 类型分别对应 Int64Var 、 UintVar、Float64Var 方法,也是同理,不再赘述。

### 字符串

定义一个 name参数,不指定默认为 jack 

```go
var name string

func init()  {
	flag.StringVar(&name, "name", "jack", "你的名字")
}

func main(){
	flag.Parse()
	fmt.Println(name)
}
```

运行后,执行结果如下

```shell
$ go run main.go 
jack
写代码的明哥's avatar
update  
写代码的明哥 已提交
208

王炳明 已提交
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
$ go run main.go --name wangbm
wangbm
```

### 时间类型

定义一个 interval 参数,不指定默认为 1s 

```go
var interval time.Duration

func init()  {
	flag.DurationVar(&interval, "interval", 1 * time.Second, "循环间隔")
}

func main(){
	flag.Parse()
	fmt.Println(interval)
}
```

验证效果如下

```shell
$ go run main.go 
1s
$ go run main.go --interval 2s
2s
```

写代码的明哥's avatar
update  
写代码的明哥 已提交
239 240 241
## 5. 自定义类型

flag 包支持的类型有 Bool、Duration、Float64、Int、Int64、String、Uint、Uint64。
王炳明 已提交
242

写代码的明哥's avatar
update  
写代码的明哥 已提交
243 244 245
这些类型的参数被封装到其对应的后端类型中,比如 Int 类型的参数被封装为 intValue,String 类型的参数被封装为 stringValue。

这些后端的类型都实现了 flag.Value 接口,因此可以把一个命令行参数抽象为一个 Flag 类型的实例。下面是 Value 接口和 Flag 类型的代码:
王炳明 已提交
246 247 248 249 250 251 252

```go
type Value interface {
	String() string
	Set(string) error
}

写代码的明哥's avatar
update  
写代码的明哥 已提交
253 254 255 256 257 258 259 260
// Flag 类型
type Flag struct {
    Name     string // name as it appears on command line
    Usage    string // help message
    Value    Value  // value as set 是个 interface,因此可以是不同类型的实例。
    DefValue string // default value (as text); for usage message
}

王炳明 已提交
261 262 263 264 265
func Var(value Value, name string, usage string) {
	CommandLine.Var(value, name, usage)
}
```

写代码的明哥's avatar
update  
写代码的明哥 已提交
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
想要实现自定义类型的参数,其实只要 Var 函数的第一个参数对象实现 flag.Value接口即可

```go
type sliceValue []string


func newSliceValue(vals []string, p *[]string) *sliceValue {
	*p = vals
	return (*sliceValue)(p)
}

func (s *sliceValue) Set(val string) error {
         // 如何解析参数值
	*s = sliceValue(strings.Split(val, ","))
	return nil
}

func (s *sliceValue) String() string {
	return strings.Join([]string(*s), ",")
}
```

比如我想实现如下效果,传入的参数是一个字符串,以逗号分隔,flag 的解析时将其转成 slice。
王炳明 已提交
289 290 291 292 293 294

```shell
$ go run demo.go -members "Jack,Tom"
[Jack Tom]
```

写代码的明哥's avatar
update  
写代码的明哥 已提交
295
那我可以这样子编写代码
王炳明 已提交
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329

```go
var members []string
type sliceValue []string


func newSliceValue(vals []string, p *[]string) *sliceValue {
	*p = vals
	return (*sliceValue)(p)
}

func (s *sliceValue) Set(val string) error {
         // 如何解析参数值
	*s = sliceValue(strings.Split(val, ","))
	return nil
}


func (s *sliceValue) String() string {
	return strings.Join([]string(*s), ",")
}

func init()  {
	flag.Var(newSliceValue([]string{}, &members), "members", "会员列表")
}

func main(){
	flag.Parse()
	fmt.Println(members)
}
```

有的朋友 可能会对 `(*sliceValue)(p)` 这行代码有所疑问,这是什么意思呢?

写代码的明哥's avatar
update  
写代码的明哥 已提交
330
关于这个,其实之前在 【[2.9 详细图解:静态类型与动态类型](http://golang.iswbm.com/en/latest/c02/c02_09.html#id2)】有讲过,忘记了可以前往复习。
王炳明 已提交
331

写代码的明哥's avatar
update  
写代码的明哥 已提交
332
## 6. 长短选项
王炳明 已提交
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383

flag 包,在使用上,其实并没有没有长短选项之别,你可以看下面这个例子

```go
package main

import (
	"flag"
	"fmt"
)

var name string

func init()  {
	flag.StringVar(&name, "name", "明哥", "你的名字")
}

func main(){
	flag.Parse()
	fmt.Println(name)
}
```

通过指定如下几种参数形式

```shell
$ go run main.go 
明哥
$ go run main.go --name jack
jack
$ go run main.go -name jack
jack
```

一个 `-` 和两个 `-` 执行结果是相同的。

那么再加一个呢?

终于报错了。说明最多只能指定两个 `-`

```shell
$ go run main.go ---name jack
bad flag syntax: ---name
Usage of /tmp/go-build245956022/b001/exe/main:
  -name string
    	你的名字 (default "明哥")
exit status 2
```



写代码的明哥's avatar
update  
写代码的明哥 已提交
384 385 386 387 388 389 390 391
## 7. 总结一下

flag 在绝大多数场景下,它是够用的,但如果要支持更多的命令传入格式,flag 可能并不是最好的选择。

那些在标准库不能解决的场景,往往会有相应的Go爱好者提供第三方解决方案。我所了解到的 cobra 就是一个非常不错的库。

它能够支持 flag 不能支持的功能,比如 **支持短选项****支持子命令** 等等,后面找个机会再好好写一下。

王炳明 已提交
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
## flag 的函数

### Lookup

从众多数参数中查取出 members 的参数值

```go
m := flag.Lookup("members")
```





![](http://image.iswbm.com/20200607174235.png)