简介

写程序总是需要一定的配置或者参数的。参数的来源可以是:命令行输入、配置文件和环境变量。本文就已经了解到Go语言的几种配置或者参数方式加一说明。 初步估计分为:

  • 原生工具获取
  • 三方工具获取

原生参数获取方式

命令行获取参数

原生的命令行获取参数有两种方式:简单粗暴的和参数解析

简单粗暴的方式直接获取 os.Args数组。

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

import (
    "fmt"
    "os"
)

func main(){
	fmt.Println("命令行的参数有", len(os.Args))
	// 遍历 os.Args 切片,就可以得到所有的命令行输入参数值
    for i, v := range os.Args {
        fmt.Printf("args[%v]=%v\n", i, v)
    }
}
/*
go run get_cmd_data_s.go -u utst -p ptest 
命令行的参数有 5
args[0]=/var/folders/rq/24z0mdzj6tj65w__1xppmzwn2w8lw2/T/go-build234466484/b001/exe/get_cmd_data_s
args[1]=-u
args[2]=utst
args[3]=-p
args[4]=ptest
*/

linux命令行参数解析方式。示例代码如下

 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

package main

import (
	"fmt"
	"flag"
)

func main(){
	// 定义几个变量,用于接收命令行的参数值
    var user        string
    var password    string
    var host        string
    var port        int
    // &user 就是接收命令行中输入 -u 后面的参数值,其他同理
    flag.StringVar(&user, "u", "root", "账号,默认为root")
    flag.StringVar(&password, "p", "", "密码,默认为空")
    flag.StringVar(&host, "h", "localhost", "主机名,默认为localhost")
    flag.IntVar(&port, "P", 3306, "端口号,默认为3306")
    // 解析命令行参数写入注册的flag里
    flag.Parse()
    // 输出结果
    fmt.Printf("user:%v\npassword:%v\nhost:%v\nport:%v\n",
        user, password, host, port)
}
/*
go run get_cmd_data_c.go   
user:root
password:
host:localhost
port:3306

go run get_cmd_data_c.go -p "test"
user:root
password:test
host:localhost
port:3306

go run get_cmd_data_c.go -p test -P2006
flag provided but not defined: -P2006
Usage of /var/folders/rq/24z0mdzj6tj65w__1xppmzwn2w8lw2/T/go-build192990242/b001/exe/get_cmd_data_c:
  -P int
        端口号,默认为3306 (default 3306)
  -h string
        主机名,默认为localhost (default "localhost")
  -p string
        密码,默认为空
  -u string
        账号,默认为root (default "root")
exit status 2

go run get_cmd_data_c.go -p test -P 2006
user:root
password:test
host:localhost
port:2006
*/

命令行获取参数,以上两种方式基本满足大部分的需求了

三方工具

viper

官方网址:https://github.com/spf13/viper 安装命令:go get -u github.com/spf13/viper

什么是 Viper

Viper是Go应用程序的完整配置解决方案,包括12-Factor应用程序。它旨在在应用程序中工作,并可以处理所有类型的配置需求和格式。它支持:

  • 设置默认值
  • 从JSON,TOML,YAML,HCL和Java属性配置文件中读取
  • 实时观看和重新读取配置文件(可选)
  • 从环境变量中读取
  • 从远程配置系统(etcd或Consul)读取,并观察变化
  • 从命令行标志读取
  • 从缓冲区读取
  • 设置显式值

Viper可以被认为是所有应用程序配置需求的注册表。

为何选择Viper

构建现代应用程序时,您不必担心配置文件格式; 你想专注于构建真棒软件。Viper就是为此提供帮助的。 Viper为您做了以下事情:

  1. 以JSON,TOML,YAML,HCL或Java属性格式查找,加载和解组配置文件。
  2. 提供一种机制来为不同的配置选项设置默认值。
  3. 提供一种机制来为通过命令行标志指定的选项设置覆盖值。
  4. 提供别名系统,轻松重命名参数,而不会破坏现有代码。
  5. 可以很容易地区分用户提供命令行或配置文件与默认值相同的时间。

Viper使用以下优先顺序。每个项目优先于其下方的项目:

  • explicit call to Set
  • flag
  • env
  • config
  • key/value store
  • default

Viper设置默认值

 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

package main

import(
	"fmt"
	"github.com/spf13/viper"
)

func main(){
	viper.SetDefault("key_a_1", "a_1")
	viper.SetDefault("key_a_2", "a_2")
	viper.SetDefault("key_b", map[string]string{"tag": "tags", "category": "categories"})
	viper.SetDefault("key_b.test","test")
	config := viper.AllSettings()
	fmt.Println(config)
	fmt.Printf("key_a_1 value is %v\n", viper.Get("key_a_1"))
	fmt.Printf("key_a_2 value is %v\n", viper.Get("key_a_2"))
	fmt.Printf("key_b value is %v\n", viper.Get("key_b"))
	fmt.Printf("key_b.tag value is %v\n", viper.Get("key_b.tag"))
	fmt.Printf("key_b.test value is %v\n", viper.Get("key_b.test"))
}

//viper_default.go

/*
go run viper_default.go
map[key_a_1:a_1 key_a_2:a_2 key_b:map[test:test]]
key_a_1 value is a_1
key_a_2 value is a_2
key_b value is map[test:test]
key_b.tag value is <nil>
key_b.test value is test
*/

Viper读取配置文件

Viper需要最少的配置,因此它知道在哪里查找配置文件。Viper支持JSON,TOML,YAML,HCL和Java Properties文件。Viper可以搜索多个路径,但目前单个Viper实例仅支持单个配置文件。Viper不会默认使用任何配置搜索路径,而是将默认值决定应用于应用程序。

以下是如何使用Viper搜索和读取配置文件的示例。不需要任何特定路径,但应在预期配置文件的位置提供至少一个路径。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

viper.SetConfigName("config") // name of config file (without extension)
viper.AddConfigPath("/etc/appname/")   // path to look for the config file in
viper.AddConfigPath("$HOME/.appname")  // call multiple times to add many search paths
viper.AddConfigPath(".")               // optionally look for config in the working directory
err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file
	panic(fmt.Errorf("Fatal error config file: %s \n", err))
}

监听并重新读取配置文件

Viper支持在运行时让应用程序实时读取配置文件。

需要重新启动服务器以使配置生效的日子已经一去不复返了,viper驱动的应用程序可以在运行时读取配置文件的更新,而不会错过任何一个节拍。 只需告诉viper实例watchConfig即可。您可以选择为Viper提供每次发生更改时运行的功能。

确保在调用之前添加所有configPath WatchConfig()

1
2
3
4
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
	fmt.Println("Config file changed:", e.Name)
})

从io.Reader读取配置

Viper预定义了许多配置源,例如文件,环境变量,标志和远程K / V存储,但您不受它们的约束。您还可以实现自己的必需配置源并将其提供给viper。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
viper.SetConfigType("yaml") // 或viper.SetConfigType(“YAML”)
//任何需要将此配置放入程序的方法
var yamlExample = []byte(`
Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
  jacket: leather
  trousers: denim
age: 35
eyes : brown
beard: true
`)

viper.ReadConfig(bytes.NewBuffer(yamlExample))

viper.Get("name")

Viper设置并覆盖配置值

1
2
3
4

viper.Set("Verbose", true)
viper.Set("LogFile", LogFile)

Viper注册和使用别名

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

1
2
3
4
5
6
7
8

viper.RegisterAlias("loud", "Verbose")
viper.Set("verbose", true) // same result as next line
viper.Set("loud", true)   // same result as prior line

viper.GetBool("loud") // true
viper.GetBool("verbose") // true

Viper使用环境变量

Viper完全支持环境变量。有四种方法可以帮助使用ENV:

1
2
3
4
5
6

AutomaticEnv()
BindEnv(string...) : error
SetEnvPrefix(string)
SetEnvKeyReplacer(string...) *strings.Replacer

BindEnv需要一个或两个参数。第一个参数是键名,第二个是环境变量的名称。环境变量的名称区分大小写。如果未提供ENV变量名,则Viper将自动假设密钥名称与ENV变量名称匹配,但ENV变量为IN ALL CAPS。当您明确提供ENV变量名称时,它不会自动添加前缀。

使用ENV变量时要认识到的一件重要事情是每次访问时都会读取该值。ViperBindEnv调用时不会修复该值。

AutomaticEnv尤其是当与结合了强大的帮手 SetEnvPrefix。调用时,Viper将在任何viper.Get请求发出时检查环境变量。它将适用以下规则。它将检查一个环境变量,其名称与大写的键匹配,并以EnvPrefix前缀。

SetEnvKeyReplacer允许您使用strings.Replacer对象重写Env键到一定程度。如果要-在Get()调用中使用或使用某些内容 ,但希望环境变量使用_分隔符,则此选项非常有用。可以在中找到使用它的示例viper_test.go

ENV实例

1
2
3
4
5
6

SetEnvPrefix("spf")  //将自动大写
BindEnv("id")
os.Setenv("SPF_ID", "13") // 通常在应用以外完成
id := Get("id") // 13

使用flag

Viper能够绑定到flag。

就像BindEnv,在调用绑定方法时,不会设置该值。这意味着您可以尽早绑定,甚至可以在init()函数中绑定 。

对于单个标志,该BindPFlag()方法提供此功能。

1
2
serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))

您还可以绑定一组现有的pflagspflag.FlagSet

1
2
3
4
5
6
7
pflag.Int("flagname", 1234, "help message for flagname")

pflag.Parse()
viper.BindPFlags(pflag.CommandLine)

i := viper.GetInt("flagname") 

Viper中使用pflag并不排除使用 标准库中使用标志包的其他包。pflag包可以通过导入这些标志来处理为标志包定义的标志。这是通过调用名为AddGoFlagSet()pflag包提供的便利函数来实现的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main
import (
	"flag"
	"github.com/spf13/pflag"
)
func main() {

	// using standard library "flag" package
	flag.Int("flagname", 1234, "help message for flagname")

	pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
	pflag.Parse()
	viper.BindPFlags(pflag.CommandLine)

	i := viper.GetInt("flagname") // retrieve value from viper

	...
}

flag接口

如果您不使用,Viper提供两个Go接口来绑定其他标志系统Pflags

FlagValue代表一个标志。这是一个关于如何实现此接口的非常简单的示例:

1
2
3
4
5
6
type myFlag struct {}
 func  f  myFlagHasChanged()bool { return  false }
 func  f  myFlagName()string { return   my-flag-name  }
 func  f  myFlagValueString()string { return   my -flag-value  }
 func  f  myFlagValueType()string { return   string  }

一旦你的flag实现了这个接口,你可以告诉Viper绑定它:

1
viper.BindFlagValue("my-flag-name", myFlag{})

远程key/value存储

要在Viper中启用远程支持,请对viper/remote 包进行空白导入:

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

Viper将读取key/value存储(如etcdConsul)中的路径检索的配置字符串(如JSONTOMLYAMLHCL)。这些值优先于默认值,但会被从磁盘,标志或环境变量检索的配置值覆盖。

Viper使用crypt从K / V存储中检索配置,这意味着您可以存储加密的配置值,并在拥有正确的gpg密钥环时自动解密。加密是可选的。

您可以将远程配置与本地配置结合使用,也可以独立使用。

crypt有一个命令行帮助程序,您可以使用它来将配置放入K / V存储区。crypthttp://127.0.0.1:4001上默认为etcd

1
2
3
4

go get github.com/xordataexchange/crypt/bin/crypt 
crypt set -plaintext /config/hugo.json /Users/hugo/settings/config.json

确认您的值已设置:

1
2
3

crypt get -plaintext /config/hugo.json

远程key/value存储示例 - 未加密

1
2
3
4
5

viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json") //因为字节流中没有文件扩展名,支持的扩展名是“json”,“toml”,“yaml”,“yml”,“properties”,“props”,“prop”
err := viper.ReadRemoteConfig()

远程key/value存储示例 - 加密

1
2
3
4
5

viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg")
viper.SetConfigType("json") //因为字节流中没有文件扩展名,支持的扩展名是“json”,“toml”,“yaml”,“yml”,“properties”,“props”,“prop” 
err := viper.ReadRemoteConfig()

监听etcd中的变化 - 未加密

 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

//或者,您可以创建一个新的viper实例
var runtime_viper = viper.New()

runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml")
runtime_viper.SetConfigType("yaml")

// 第一次从远程配置中读取
err := runtime_viper.ReadRemoteConfig()

//解密配置
runtime_viper.Unmarshal(&runtime_conf)

// 打开一个goroutine来永远监听远程变化
go func(){
	for {
	    time.Sleep(time.Second * 5) // 每次请求后延迟
	    err := runtime_viper.WatchRemoteConfig()
	    if err != nil {
	        log.Errorf("unable to read remote config: %v", err)
	        continue
	    }

	    //将新配置解组到我们的运行时配置结构中。您还可以使用通道
        //实现信号以通知系统更改 
	    runtime_viper.Unmarshal(&runtime_conf)
	}
}()

Viper获取值

在Viper中,有几种方法可以根据值的类型获取值。存在以下功能和方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

Get(key string) : interface{}
GetBool(key string) : bool
GetFloat64(key string) : float64
GetInt(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
5
6

viper.GetString("logfile") // case-insensitive Setting & Getting
if viper.GetBool("verbose") {
    fmt.Println("verbose enabled")
}

参考

  1. Go:获取命令行参数
  2. github.com/spf13/viper go viper包介绍