在介绍 Viper 库的那一篇文章中我们有提到过 viper 在设置键值时的优先级,依次是 调用Set显示设置的 > 命令行选项 > 环境变量 > 配置文件 > 默认值。

在实际项目开发中一般不会直接 Set 来设置,因为要使用的配置现象过多。因此,在考虑直接读取配置文件之前,我们要先做一件事,那就是解析命令行,看看有没有相应的键值设置。

Go语言内置的flag包实现了命令行参数的解析,flag包使得开发命令行工具更为简单。

如何获取命令行参数?

如果只是简单的想要获取命令行参数,可以直接使用 os.Args 来获取命令行参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)
}
}
}

将上面的代码执行go build -o "args_demo"编译之后,执行:

1
2
3
4
5
6
$ ./args_demo a b c d
args[0]=./args_demo
args[1]=a
args[2]=b
args[3]=c
args[4]=d

os.Args是一个存储命令行参数的字符串切片,它的第一个元素是执行文件的名称。

Flag

flag用于解析命令行选项。

命令行选项在实际开发中很常用,特别是在写工具的时候。

  • 指定配置文件的路径,如redis-server ./redis.conf以当前目录下的配置文件redis.conf启动 Redis 服务器;
  • 自定义某些参数,如python -m SimpleHTTPServer 8080启动一个 HTTP 服务器,监听 8080 端口。如果不指定,则默认监听 8000 端口。

基本使用

flag包支持的命令行参数类型有boolintint64uintuint64float float64stringduration

flag参数 有效值
字符串flag 合法字符串
整数flag 1234、0664、0x1234等类型,也可以是负数。
浮点数flag 合法浮点数
bool类型flag 1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False。
时间段flag 任何合法的时间段字符串。如”300ms”、”-1.5h”、“2h45m”。合法的单位有”ns”、“us” /“µs”、“ms”、“s”、“m”、“h”。

flag.Type()

基本格式如下:

flag.Type(flag名, 默认值, 帮助信息)*Type 例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:

1
2
3
4
name := flag.String("name", "张三", "姓名")
age := flag.Int("age", 18, "年龄")
married := flag.Bool("married", false, "婚否")
delay := flag.Duration("d", 0, "时间间隔")

需要注意的是,此时nameagemarrieddelay均为对应类型的指针。

flag.TypeVar()

基本格式如下: flag.TypeVar(Type指针, flag名, 默认值, 帮助信息) 例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:

1
2
3
4
5
6
7
8
var name string
var age int
var married bool
var delay time.Duration
flag.StringVar(&name, "name", "张三", "姓名")
flag.IntVar(&age, "age", 18, "年龄")
flag.BoolVar(&married, "married", false, "婚否")
flag.DurationVar(&delay, "d", 0, "时间间隔")

flag.Parse()

通过以上两种方法定义好命令行flag参数后,需要通过调用flag.Parse()来对命令行参数进行解析。

支持的命令行参数格式有以下几种:

  • -flag xxx (使用空格,一个-符号)
  • --flag xxx (使用空格,两个-符号)
  • -flag=xxx (使用等号,一个-符号)
  • --flag=xxx (使用等号,两个-符号)

其中,布尔类型的参数必须使用等号的方式指定。

遇到第一个非选项参数(即不是以---开头的)或终止符--,解析停止。


总结一下,使用flag库的一般步骤:

  • 定义一些全局变量存储选项的值;
  • init方法中使用flag.TypeVar方法定义选项,这里的Type可以为基本类型Int/Uint/Float64/Bool,还可以是时间间隔time.Duration。定义时传入变量的地址、选项名、默认值和帮助信息;
  • main方法中调用flag.Parseos.Args[1:]中解析选项。因为os.Args[0]为可执行程序路径,会被剔除。

注意点:

flag.Parse方法必须在所有选项都定义之后调用,且flag.Parse调用之后不能再定义选项。如果按照前面的步骤,基本不会出现问题。 因为init在所有代码之前执行,将选项定义都放在init中,main函数中执行flag.Parse时所有选项都已经定义了。

flag其他函数

1
2
3
flag.Args()  ////返回命令行参数后的其他参数,以[]string类型
flag.NArg() //返回命令行参数后的其他参数个数
flag.NFlag() //返回使用的命令行参数个数

高级用法

定义短选项

flag库并没有显示支持短选项,但是可以通过给某个相同的变量设置不同的选项来实现。即两个选项共享同一个变量。 由于初始化顺序不确定,必须保证它们拥有相同的默认值。否则不传该选项时,行为是不确定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"flag"
)

var logLevel string

func init() {
const (
defaultLogLevel = "DEBUG"
usage = "set log level value"
)

flag.StringVar(&logLevel, "log_type", defaultLogLevel, usage)
flag.StringVar(&logLevel, "l", defaultLogLevel, usage + "(shorthand)")
}

func main() {
flag.Parse()

fmt.Println("log level:", logLevel)
}

自定义选项

除了使用flag库提供的选项类型,我们还可以自定义选项类型。我们分析一下标准库中提供的案例:

1
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
34
35
36
37
38
39
40
41
42
43
package main

import (
"errors"
"flag"
"fmt"
"strings"
"time"
)

type interval []time.Duration

func (i *interval) String() string {
return fmt.Sprint(*i)
}

func (i *interval) Set(value string) error {
if len(*i) > 0 {
return errors.New("interval flag already set")
}
for _, dt := range strings.Split(value, ",") {
duration, err := time.ParseDuration(dt)
if err != nil {
return err
}
*i = append(*i, duration)
}
return nil
}

var (
intervalFlag interval
)

func init() {
flag.Var(&intervalFlag, "deltaT", "comma-seperated list of intervals to use between events")
}

func main() {
flag.Parse()

fmt.Println(intervalFlag)
}

首先定义一个新类型,这里定义类型interval

新类型必须实现flag.Value接口:

1
2
3
4
5
// src/flag/flag.go
type Value interface {
String() string
Set(string) error
}

其中String方法格式化该类型的值,flag.Parse方法在执行时遇到自定义类型的选项会将选项值作为参数调用该类型变量的Set方法。 这里将以,分隔的时间间隔解析出来存入一个切片中。

自定义类型选项的定义必须使用flag.Var方法。

解析程序中的字符串

有时候选项并不是通过命令行传递的。例如,从配置表中读取或程序生成的。这时候可以使用flag.FlagSet结构的相关方法来解析这些选项。

实际上,我们前面调用的flag库的方法,都会间接调用FlagSet结构的方法。flag库中定义了一个FlagSet类型的全局变量CommandLine专门用于解析命令行选项。 前面调用的flag库的方法只是为了提供便利,它们内部都是调用的CommandLine的相应方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/flag/flag.go
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

func Parse() {
CommandLine.Parse(os.Args[1:])
}

func IntVar(p *int, name string, value int, usage string) {
CommandLine.Var(newIntValue(value, p), name, usage)
}

func Int(name string, value int, usage string) *int {
return CommandLine.Int(name, value, usage)
}

func NFlag() int { return len(CommandLine.actual) }

func Arg(i int) string {
return CommandLine.Arg(i)
}

func NArg() int { return len(CommandLine.args) }

同样的,我们也可以自己创建FlagSet类型变量来解析选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"flag"
"fmt"
)

func main() {
args := []string{"-intflag", "12", "-stringflag", "test"}

var intflag int
var boolflag bool
var stringflag string

fs := flag.NewFlagSet("MyFlagSet", flag.ContinueOnError)
fs.IntVar(&intflag, "intflag", 0, "int flag value")
fs.BoolVar(&boolflag, "boolflag", false, "bool flag value")
fs.StringVar(&stringflag, "stringflag", "default", "string flag value")

fs.Parse(args)

fmt.Println("int flag:", intflag)
fmt.Println("bool flag:", boolflag)
fmt.Println("string flag:", stringflag)
}

NewFlagSet方法有两个参数,第一个参数是程序名称,输出帮助或出错时会显示该信息。第二个参数是解析出错时如何处理,有几个选项:

  • ContinueOnError:发生错误后继续解析,CommandLine就是使用这个选项;
  • ExitOnError:出错时调用os.Exit(2)退出程序;
  • PanicOnError:出错时产生 panic。

随便看一眼flag库中的相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/flag/flag.go
func (f *FlagSet) Parse(arguments []string) error {
f.parsed = true
f.args = arguments
for {
seen, err := f.parseOne()
if seen {
continue
}
if err == nil {
break
}
switch f.errorHandling {
case ContinueOnError:
return err
case ExitOnError:
os.Exit(2)
case PanicOnError:
panic(err)
}
}
return nil
}

与直接使用flag库的方法有一点不同,FlagSet调用Parse方法时需要显示传入字符串切片作为参数。因为flag.Parse在内部调用了CommandLine.Parse(os.Args[1:])

总结

至此,本项目的解析配置文件功能已经基本完成了,该功能先是使用 flag 库来解析命令行中的内容,当命令行内容不为空时,则使用输入值来进行配置;若为空,则通过解析原有的配置文件来实现。

参考