命令行参数解析flag包和配置管理viper包

os.Args

os包中提供命令行接收后续参数,但是使用比较简单。

缺点是无法解析参数,只能通过位置值获取,内置的flag包则可以实现命令行参数的解析

1
2
3
4
5
6
7
8
9
    for i := range os.Args {
fmt.Println(os.Args[i])
}

go run main.go 1 2 3 4
1
2
3
4

flag

flag包支持的命令行参数类型:bool int int64 uint uint64 float64 string duration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
b := flag.Bool("bool", false, "bool类型")
i := flag.Int("int", 0, "int类型")
i64 := flag.Int64("int64", 0, "int64类型")
u := flag.Uint("uint", 0, "uint类型")
u64 := flag.Uint64("uint64", 0, "uint64类型")
f64 := flag.Float64("float64", 0.0, "float64类型")
s := flag.String("string", "", "string类型")
d := flag.Duration("duration", time.Second, "时间类型")

// 语法分析
flag.Parse()

// 也可以类似os.Args接收参数
fmt.Printf("所有参数:%v,参数个数:%d\n", flag.Args(), flag.NArg())
for i := 0; i < flag.NArg(); i++ {
fmt.Printf("第%d个参数,值是%v\n", i, flag.Arg(i))
}
fmt.Printf("bool: %t,int: %d,int64: %d,uint: %d,uint64: %d,float64: %f,string: %s,duration: %v\n",
*b, *i, *i64, *u, *u64, *f64, *s, *d)

输出

1
2
3
4
5
go run main.go --int64=64 --uint64=164 --bool=true --int=123 --uint=2 --float64=1.7 --string=a --duration=10s arg1

所有参数:[arg1],参数个数:1
第0个参数,值是arg1
bool: true,int: 123,int64: 64,uint: 2,uint64: 164,float64: 1.700000,string: a,duration: 10s

viper

适用于Go应用程序的完整配置解决方案。(不全面,这里列举几个经常会用到的特性,更全面的特性可以查看官方RM文档):

  • 设置默认配置
  • 从JSON,YAML格式的配置文件读取配置信息
  • 从环境变量中读取
  • 从命令行参数读取
  • 显示配置值

安装

1
go get github.com/spf13/viper

参数设置优先级,从高到低:

  • 显示调用Set设置值
  • 命令行参数(flag)
  • 环境变量
  • 配置文件
  • Key/value存储
  • 默认值

viper的用法是将配置存入viper,使用时,从viper读出来

1
2
viper.Set("service", "viper")
viper.Get("service") // 返回viper

将值存入viper

设置值

1
viper.Set("service", "viper")

默认值

1
viper.SetDefault("service", "abc")

通过配置文件

viper可以从多个目录搜索各种格式的配置文件

1
2
3
4
5
6
7
8
viper.SetConfigFile("./config.yaml") // 直接指定

viper.SetConfigName("config") // 配置文件名称(无扩展名)
viper.SetConfigType("yaml") // 如果配置文件的名称中没有扩展名,则需要配置此项
viper.AddConfigPath("/etc/xxx/") // 查找配置文件所在的路径
viper.AddConfigPath(".") // 多次调用以添加多个搜索路径

err := viper.ReadInConfig() // 查找并读取配置文件,返回的报错可能是没找到,也可能是找到了但是有其他的错误

监控并重新读取配置文件

Viper支持在运行时实时读取配置文件的功能。通过回调函数,实现改动之后的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
viper.SetConfigFile("./config.yaml")
go func() {
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
// 配置文件发生变更之后会调用的回调函数
fmt.Println("Config file changed:", e.Name, e.Op)
err := viper.ReadInConfig()
if err != nil {
log.Fatal(err)
}
fmt.Println(viper.Get("service"))
})
}()

改动之后

1
2
Config file changed: config.yaml WRITE|CHMOD
viper12345

从io.Reader读取配置

viper可以通过读取的字符串中的获取配置

1
2
3
4
5
6
7
8
9
10
11
f, err := os.Open("./config.yaml")
if err != nil {
log.Fatal(err)
}
defer f.Close()
viper.SetConfigType("yaml")
err = viper.ReadConfig(f)
if err != nil {
log.Fatal(err)
}
fmt.Println(viper.Get("service"))

别名

别名允许多个键引用一个值

1
2
3
viper.RegisterAlias("service", "anotherService")
viper.Set("service", "viper")
fmt.Println(viper.Get("anotherService"))

使用环境变量

1
2
3
4
5
6
7
SetEnvPrefix(string) 设置环境变量前缀
SetEnvKeyReplacer(string...) *strings.Replacer 环境变量字符串变更

AutomaticEnv() 在get时,自动获取对应的环境变量
BindEnv(string...) : error 绑定环境变量的值到viper中

AllowEmptyEnv(bool) 空环境变量,默认为false
1
2
3
4
5
6
7
8
9
viper.AutomaticEnv()                            // 自动绑定
viper.SetEnvPrefix("vip")
os.Setenv("VIP_ID", "13")
fmt.Println(viper.Get("id")) // 自动绑定之后获取 // 13
replacer := strings.NewReplacer("VIP", "NVIP") // 将获取的前缀从VIP改成NVIP
viper.SetEnvKeyReplacer(replacer)
os.Setenv("NVIP_ID", "14")
viper.BindEnv("id")
fmt.Println(viper.Get("id")) // 14

使用Pflag

类似于falg,在命令行中通过参数传递。例如在Cobra库中使用Pflag

1
2
3
4
5
6
7
8
9
10
11
12
13
var rootCmd = &cobra.Command{
Use: "hugo",
Short: "Hugo is a very fast static site generator",
Long: `A Fast and Flexible Static Site Generator built with
love by spf13 and friends in Go.
Complete documentation is available at http://hugo.spf13.com`,
Run: func(cmd *cobra.Command, args []string) {
// Do Stuff Here
},
}
rootCmd.Flags().Int("port", 1138, "Port to run Application server on") // 当如果没有参数传入,使用默认值1138
viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
fmt.Println(viper.Get("port"))

或者直接使用pflag

1
2
3
4
pflag.String("service", "viper", "this is service name")
pflag.Parse()
viper.BindPFlags(pflag.CommandLine)
fmt.Println(viper.Get("service"))

也可以自己实现flag接口,具体实现方式在详细说明文档中。

远程Key/Value存储

在Viper中启用远程支持,需要在代码中匿名导入viper/remote这个包。

1
import _ "github.com/spf13/viper/remote"

Viper可以从etcdConsul中读取配置,并且设置加密。在etcd中可以监控配置值是否被修改。

将配置写入文件

viper支持当配置改动之后,将配置写入到文件,可以安全的写入,也就是不覆盖,只新建

1
2
3
4
5
6
7
8
    viper.AddConfigPath("./")
viper.SetConfigName("config2")
viper.SetConfigType("yaml")

viper.WriteConfig() // 将当前配置写入“viper.AddConfigPath()”和“viper.SetConfigName”设置的预定义路径
viper.SafeWriteConfig()
viper.WriteConfigAs("config.yaml")
viper.SafeWriteConfigAs("./config") // 因为该配置文件写入过,所以会报错 Config File "xxx/config2.yaml" Already Exists

从Viper中获取值

可以通过值的类型获取值

1
2
3
4
5
6
7
8
9
10
11
12
13
Get(key string) : interface{}
GetBool(key string) : bool
GetFloat64(key string) : float64
GetInt(key string) : int
GetIntSlice(key string) : []int
GetString(key string) : string
GetStringMap(key string) : map[string]interface{}
GetStringMapString(key string) : map[string]string
GetStringSlice(key string) : []string
GetTime(key string) : time.Time
GetDuration(key string) : time.Duration
IsSet(key string) : bool
AllSettings() : map[string]interface{}

每一个Get方法在找不到值的时候都会返回零值。为了检查给定的键是否存在,提供了IsSet()方法。

1
2
3
4
viper.Set("service", "viper")
if viper.IsSet("service") {
fmt.Println(viper.Get("service"))
}

嵌套数据访问

1
2
viper.Set("service.name", "viper")
fmt.Println(viper.Get("service")) // map[name:viper]

这遵守上面建立的优先规则;搜索路径将遍历其余配置注册表,直到找到为止。(译注:因为Viper支持从多种配置来源,例如磁盘上的配置文件>命令行标志位>环境变量>远程Key/Value存储>默认值,我们在查找一个配置的时候如果在当前配置源中没找到,就会继续从后续的配置源查找,直到找到为止。)

例如,在给定此配置文件的情况下,datastore.metric.hostdatastore.metric.port均已定义(并且可以被覆盖)。如果另外在默认值中定义了datastore.metric.protocol,Viper也会找到它。

然而,如果datastore.metric被直接赋值覆盖(被flag,环境变量,set()方法等等…),那么datastore.metric的所有子键都将变为未定义状态,它们被高优先级配置级别“遮蔽”(shadowed)了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"datastore.metric.host": "0.0.0.0",
"host": {
"address": "localhost",
"port": 5799
},
"datastore": {
"metric": {
"host": "127.0.0.1",
"port": 3099
},
"warehouse": {
"host": "198.0.0.1",
"port": 2112
}
}
}

GetString("datastore.metric.host") // 返回 "0.0.0.0"

反序列化

viper支持直接将配置反序列化到结构体对象中

1
2
Unmarshal(rawVal interface{}) : error
UnmarshalKey(key string, rawVal interface{}) : error
1
2
3
4
5
6
7
8
9
10
11
12
viper.Set("service.name", "viper")
var s = struct {
Service struct {
// 注意,这里的tag是mapstructure
Name1 string `mapstructure:"name"`
}
}{}
err := viper.Unmarshal(&s)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", s)

输出

1
{Service:{Name1:viper}}

将所有的配置序列化成字符串,后续可以反序列化成不同的格式

1
2
3
4
5
6
viper.Set("service.name", "viper")
viper.Set("service.type", "test")
s := viper.AllSettings()
fmt.Printf("%+v\n", s)
bytes, _ := yaml.Marshal(s)
fmt.Println(string(bytes))

输出

1
2
3
4
map[service:map[name:viper type:test]]
service:
name: viper
type: test

用法推荐

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
pflag.String("service.name", "viper", "this is service name") // 设置pflag默认值

// 如果没有指定,就使用配置文件中默认配置
var configFile string
pflag.StringVar(&configFile, "configFile", "./config.yaml", "server config file")
pflag.Parse() // 命令行参数也可以传配置文件路径

// 绑定配置文件
if configFile != "" {
viper.SetConfigFile(configFile)
err := viper.ReadInConfig()
if err != nil && !os.IsNotExist(err) {
fmt.Println("config file: ", err)
}
}

// 绑定cmd参数
_ = viper.BindPFlags(pflag.CommandLine)

// 绑定环境变量
viper.SetEnvPrefix("viper")
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // 将.替换成_解析,VIPER.xxx和VIPER_xxx都可以解析

// 解析绑定的参数
common.Service.Name = viper.GetString("service.name")

同时支持命令行参数、环境变量、配置文件,参数优先级是命令行大于环境变量大于配置文件。

推荐阅读

推荐阅读中的文档更加详细:

官方文档
更详细文档:李文周的博客