Go应用构建工具(2)--viper

Go应用构建工具(2)–viper

1. 概述

基本上所有的后端应用,都是需要用到配置项,可能小的项目配置项不多会选择命令行来传递,但是对于大项目来说,配置项可能会很多,全都用命令行传递那就麻烦死了,而且不好维护。
所以基本上都是会选择将配置项保存在配置文件中,在程序启动时加载和解析。
而Viper是Go生态中目前最受欢迎的配置相关的包,Viper能满足我们对配置的各种需求,能处理不同格式的配置文件。

viper支持:

  • 设置默认值
  • JSON,TOMAL,YAML,HCL,envfile和java properties等配置文件中读取配置信息
  • 实时监控和重新读取配置文件(可选)
  • 从环境变量中读取
  • 从远程配置系统(etcd或Consul)中获取配置信息,并监控变化
  • 从命令行中读取配置
  • 从buffer中读取配置
  • 显示配置值

由上面可知,viper支持从不同的位置读取配置,但不同的位置是具有不同的优先级的,优先级高的配置会覆盖掉优先级低的相同配置,以下是优先级的排序(从高到低):

  • 显式调用Set方法设置
  • 命令行flag
  • 环境变量
  • 配置文件
  • key/value存储
  • 默认值

注意:目前的Viper配置的Key是不区分大小写的。(目前正在讨论是否将这一选项设置为可选)

2. 使用Viper

  1. 读取配置文件
    Viper读取配置文件,至少需要知道去哪里查找配置文件,它支持搜索多个路径,但当前一个Viper实例值支持单个配置文件;
    Viper默认不配置任何搜索路径,将决策权交给应用程序;
    Viper支持JSON,TOML,YAML,HCL,INI,envfile和java properties等配置文件
    示例如下:
    首先在当前目录下创建一个config目录,在config目录下新建一个config.yaml文件:
mysql:
    database: persons
    password: 123456
    port: 3306
    url: 127.0.0.1
    user: root
redis:
    port: 6379
    url: 127.0.0.1
root:
    loglevel: debug
    name: app
    port: 8080
package main

import (
	"fmt"

	"github.com/spf13/viper"
)

func main() {
	viper.SetConfigName("config")         // 指定配置文件名(没有扩展名)
	viper.SetConfigType("yaml")           // 如果配置文件没有扩展名,则需要指定配置文件的格式,让viper知道如何解析文件
	viper.AddConfigPath("/etc/appname/")  // 指定搜索路径
	viper.AddConfigPath("$HOME/.appname") // 可以调用多次,指定多个搜索路径
	viper.AddConfigPath("./config")       // 当前工作目录下的config文件夹

    // 也可以使用SetConfigFile直接指定(上面的都不需要了)
    // viper.SetConfigFile("./config/config.yaml")

	err := viper.ReadInConfig()           // 查找并读取配置文件
	if err != nil {                       // 处理错误
		panic(fmt.Errorf("fatal error config file: %w", err))
	}

    fmt.Printf("redis.Port = %v\n", viper.Get("redis.port"))
}

这里有几个点要注意:

  1. Viper支持配置多个搜索路径,但是需要注意添加的顺序,viper会根据添加的路径顺序搜索,如果找到了则停止搜索。
  2. 如果直接使用SetConfigFile指定了配置文件路径和名字,必须显示带上文件扩展名,否则无法解析
  3. SetConfigName设置的配置文件名是不带扩展名的,在搜索时viper会加上扩展名
  4. 使用SetConfigName时,如果同一个目录下存在两个同名,但扩展名不一样的配置文件,比如config目录下存在:config.json和config.yaml,viper加载时只会是config.json文件;如果设置的扩展名是yaml,那么则是将config.json文件按照yaml解析

对于第4点的原因,跟踪了下源码,有一个切片,viper在加载时会按顺序匹配:var SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "tfvars", "dotenv", "env", "ini"}

func (v *Viper) searchInPath(in string) (filename string) {
	v.logger.Debug("searching for config in path", "path", in)
	for _, ext := range SupportedExts {
		v.logger.Debug("checking if file exists", "file", filepath.Join(in, v.configName+"."+ext))
		if b, _ := exists(v.fs, filepath.Join(in, v.configName+"."+ext)); b {
			v.logger.Debug("found file", "file", filepath.Join(in, v.configName+"."+ext))
			return filepath.Join(in, v.configName+"."+ext)
		}
	}

	if v.configType != "" {
		if b, _ := exists(v.fs, filepath.Join(in, v.configName)); b {
			return filepath.Join(in, v.configName)
		}
	}

	return ""
}

对于配置文件没找到的错误处理也可以像这样子:

if err := viper.ReadInConfig(); err != nil {
	if _, ok := err.(viper.ConfigFileNotFoundError); ok {
		// Config file not found; ignore error if desired
	} else {
		// Config file was found but another error was produced
	}
}

// Config file found and successfully parsed

Note:
从1.6版本起配置文件可以不带有扩展名,通过编程方式指定。譬如对于home目录下的配置文件是没有扩展名的,比如.bashrc

  1. 写入配置文件
    通常我们使用配置文件大部分是用于读取配置,但是有时候希望保存运行时所做的修改,因此Viper提供了以下几个方法:
  • WriteConfig:将当前的viper配置写入预定义的路径(如果路径存在),这个方法会覆盖当前的配置文件(如果文件存在),预定义路径不存在则会报错
  • SafeWriteConfig:与WriteConfig功能相似,不同之处是如果配置文件存在则不会覆盖
  • WriteConfigAs:将当前的viper配置写入到指定的文件路径,如果文件存在则会覆盖
  • SafeWriteConfigAs:与WriteConfigAs功能相似,不同之处是如果文件不存在不会覆盖

Note:带有safe的方法不会覆盖文件,当文件不存在时会新建
示例如下:

viper.WriteConfig() // 将当前配置写入“viper.AddConfigPath()”和“viper.SetConfigName”设置的预定义路径
viper.SafeWriteConfig()
viper.WriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.config") // 因为该配置文件写入过,所以会报错
viper.SafeWriteConfigAs("/path/to/my/.other_config")
  1. 建立默认值
    Viper支持value默认值(key不需要默认值)。
    示例如下:
viper.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"}
  1. 监听和重新读取配置文件
    viper支持在运行时实时读取配置文件(就是热加载)。
    只需要简单的告诉viper实例watchConfig即可监听,另外也可以提供一个回调方法给viper,用于每次有变动调用。

注意:在调用WatchConfig()之前需要确认已经添加了配置文件的搜索路径
以下是示例:

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

PS:回调方法里的参数fsnotify.Event只有改动的文件名和操作两个字段,感觉获取了也没啥用?
不太建议使用,比如修改了端口号,服务没有重启,服务还是监听在老的端口上,反而造成混淆

  1. 从io.Reader中读取配置
    viper预先定义了一些配置源,比如配置文件,环境变量,flag和远程kv存储,但是我们还可以不使用这些,使用自己的配置源提供给viper。
    具体看下示例(感觉用的也不多):
viper.SetConfigType("yaml") // or viper.SetConfigType("YAML")

// any approach to require this configuration into your program.
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") // this would be "steve"
  1. 显示设置配置
    viper通过viper.Set()函数来进行显示设置配置,这种方式的优先级是最高的!
    比如:viper.Set("redis.port",6666)

  2. 注册和使用别名
    别名允许一个值对用多个键

viper.RegisterAlias("loud", "Verbose") // 给Verbose注册一个别名loud

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
  1. 使用环境变量
    viper支持环境变量,提供了以下5个方法来操作环境变量:
  • AutomaticEnv()
  • BindEnv(string...): error
  • SetEnvPrefix(string)
  • SetEnvKeyReplacer(string...) *strings.Replacer
  • AllowEmptyEnv(bool)

warning 注意
需要注意一点,使用环境变量时,viper是区分大小写的
viper提供了一种机制保证了环境变量是唯一的:SetEnvPrefix,通过SetEnvPrefix,可以告诉viper在读取环境变量时加上指定的前缀,BindEnvAutomaticEnv也会使用这个前缀。

BindEnv有一个或多个参数,第一个参数是key名称,剩下的参数代表环境变量绑定到这个key,环境变量名称区分大小写;
如果环境变量的名称没有提供,那么viper会自动的认为环境变量匹配以下规则:prefix + _ + key name全大写,比如前缀为viper,BindEnv第一个参数为username,那么绑定的环境变量就是:VIPER_USERNAME;
如果提供了环境变量的名称(第二个参数),那么viper不会自动添加前缀,比如第二个参数是id,那么viper将会查找环境变量ID

一个重要的事情需要注意:每次访问环境变量的值时都会读取它,viper并不会在调用BindEnv时将值固定

AutomaticEnv是一个强大的助手,特别是配合SetEnvPrefix一起使用时。
当调用viper.Get时,viper会随时检查环境变量,它遵循这个规则:它会检查环境变量名称是否与key的大写名称匹配(如果key有设置了前缀则带上前缀)

SetEnvKeyReplacer允许我们使用strings.Replacer在一定程度上去重写环境变量的Key,比如当我们希望使用Get方法时用-分隔,但环境变量是以_分隔的,这种情况会比较有用。
比如,我们有环境变量USER_NAME=zhangsan,但是我们在调用viper.Get时希望是这样调用:viper.Get("user-name),那我们就可以这样子调用方法:

viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) //会用_替换-
viper.Get("user-name")

一般来说空的环境变量会被认为是未设置的,并返回到下一个配置源,如果要将空的环境变量认为是已设置的,那么就可以使用AllowEmptyEnv方法

os.Setenv("VIPER_USER_NAME", "zhangsan")

viper.AutomaticEnv()
viper.SetEnvPrefix("VIPER")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
viper.BindEnv("user-name")

fmt.Println("user-name = ", viper.Get("user-name"))
  1. 使用flag
    viper可以绑定flag,确切的说,viper支持在cobra库中使用pflag。
    BindEnv类似,当调用绑定方法时不会设置值,而是在访问它时才会设置,这意味我们可以根据需要尽早绑定,即使是在init方法中。
    对于单个flag,使用BindPflag()方法,比如:
var port = pflag.Int("port", 8888, "Input redis port")

func main() {
	// 读取配置文件
	// viper.SetConfigFile("./config/config.yaml") // 指定配置文件路径
	viper.SetConfigName("config")   // 配置文件名称(无扩展名)
	viper.SetConfigType("yaml")     // 如果配置文件的名称中没有扩展名,需要此配置
	viper.AddConfigPath("/home")    // 设置配置文件搜索路径
	viper.AddConfigPath("./config") // 设置多个搜索路径

	viper.SetDefault("redis.port", 65536)

	// viper.Set("redis.port", 6666)
	// 查找并读取配置文件
	err := viper.ReadInConfig()
	if err != nil {
		panic(fmt.Errorf("Fatal error config file: %w", err))
	}

	pflag.Parse()
	viper.BindPFlag("redis.port", pflag.CommandLine.Lookup("port"))

	redisPort := viper.Get("redis.port")
	fmt.Println("redis.port = ", redisPort)
}

也可以绑定一组现有的pflags(flag.FlagSet)

var port = pflag.Int("redis.port", 8888, "Input redis port")
var url = pflag.String("redis.url", "127.0.0.1", "Input redis url")

func main() {
	// 读取配置文件
	viper.SetConfigName("config")   // 配置文件名称(无扩展名)
	viper.SetConfigType("yaml")     // 如果配置文件的名称中没有扩展名,需要此配置
	viper.AddConfigPath("/home")    // 设置配置文件搜索路径
	viper.AddConfigPath("./config") // 设置多个搜索路径

	viper.SetDefault("redis.port", 65536)

	// viper.Set("redis.port", 6666)
	// 查找并读取配置文件
	err := viper.ReadInConfig()
	if err != nil {
		panic(fmt.Errorf("Fatal error config file: %w", err))
	}

	pflag.Parse()

	viper.BindPFlags(pflag.CommandLine)

	redisPort := viper.Get("redis.port")
	redisUrl := viper.GetString("redis.url")

	fmt.Println("redis.port = ", redisPort)
	fmt.Println("redis.port = ", redisUrl)
}

执行如下:

lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run my_viper.go
redis.port =  63793
redis.port =  127.0.0.1
lucas@Z-NB-0406:~/workspace/test/pflagtest$ go run my_viper.go --redis.port 6666 --redis.url 10.2.3.222
redis.port =  6666
redis.port =  10.2.3.222

在viper中使用pflag并不会影响其他包使用标准库的flag包,pflag包可以通过导入来处理flag包定义的flag,这是通过pflag包提供的AddGoFlagSet()实现的(pflag那篇文章有介绍)

  1. viper从远处kv存储获取(这部分目前我没etcd/Consul环境,暂时没去学习)

  2. 从Viper中获取值
    viper提供了一些方法去获取值,这些方法根据值的类型区分,主要存在以下方法:

  • Get(key string) interface{}
  • Get<Type>(key string) <Type>,比如GetString(),GetBool()
  • AllSettings() map[string]interface{}
  • IsSet(key string) bool

需要注意的一个点是:每一个Get方法当找不到key时都会返回对应类型的零值,要检查key是否存在,可以使用IsSet()方法

  1. viper的读取key值的几种情况
  • 访问嵌套的key
    比如有以下JSON配置文件:
{
    "host": {
        "address": "localhost",
        "port": 5799
    },
    "datastore": {
        "metric": {
            "host": "127.0.0.1",
            "port": 3099
        },
        "warehouse": {
            "host": "198.0.0.1",
            "port": 2112
        }
    }
}

viper支持使用.来嵌套访问,比如:GetString("datastore.metric.host")
这遵循上面建立的规则,搜索路径将按优先级遍历其余配置,直到找到为止,比如在当前配置文件中没找到,就会继续向后查找,比如查找默认值

比如,在上面的配置文件中,同时定义了datastore.metric.hostdatastore.metric.port(可以被覆盖),如果在默认值中定义了datastore.metric.protocol,viper也会找到它
然而,如果datastore.metric被直接覆盖了(flag,环境变量或者调用Set()等等),那么它的所有子key都会变成未定义,它们被高优先级别的配置遮盖了。

viper还可以通过使用number访问数组索引,比如:

{
    "host": {
        "address": "localhost",
        "ports": [
            5799,
            6029
        ]
    },
    "datastore": {
        "metric": {
            "host": "127.0.0.1",
            "port": 3099
        },
        "warehouse": {
            "host": "198.0.0.1",
            "port": 2112
        }
    }
}

使用: viper.GetInt("host.ports.1") // 返回6029

最后,如果存在于分隔的键路径匹配的键,则直接返回这个键的值,比如:

{
    "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") // returns "0.0.0.0"
  • 提取子树
    在开发可重用模块时,提取配置的子集并将其传递给模块通常是有用的。
    通过这种方式,模块可以使用不同的配置进行多次实例化。
    比如,一个应用可能使用多个不同的cache配置
cache:
  cache1:
    max-items: 100
    item-size: 64
  cache2:
    max-items: 200
    item-size: 80

假如我们现在有一个NewCache方法:

func NewCache(v *Viper) *Cache {
    return &Cache {
        MaxItems: v.GetInt("max-items"),
        ItemSize: v.GetInt("item-size"),
    }
}

那么我们就可以通过提取子树获取对应配置传递给这个NewCache方法了:

cache1Config := viper.Sub("cache.cache1")
if cache1Config == nil {
    panic("cache configuration not found")
}

cache1 := NewCache(cache1Config)
  • 反序列化
    viper支持将配置解析到struct或map等等,可以使用以下两个方法:
    • Unmarshal(rawVal interface{}) error
    • UnmarshalKey(key string, rawVal interface{}) error

比如:

type config struct {
	Port int
	Name string
	PathMap string `mapstructure:"path_map"`
}

var C config

err := viper.Unmarshal(&C)
if err != nil {
	t.Fatalf("unable to decode into struct, %v", err)
}

如果想要解析的那些键刚好包含了.(默认的键分隔符)的配置,就需要修改分隔符了:

v := viper.NewWithOptions(viper.KeyDelimiter("::"))

v.SetDefault("chart::values", map[string]interface{}{
	"ingress": map[string]interface{}{
		"annotations": map[string]interface{}{
			"traefik.frontend.rule.type":                 "PathPrefix",
			"traefik.ingress.kubernetes.io/ssl-redirect": "true",
		},
	},
})

type config struct {
	Chart struct{
		Values map[string]interface{}
	}
}

var C config

v.Unmarshal(&C)

viper还支持解析到嵌入的结构中:

/*
Example config:

module:
    enabled: true
    token: 89h3f98hbwf987h3f98wenf89ehf
*/
type config struct {
	Module struct {
		Enabled bool

		moduleConfig `mapstructure:",squash"`
	}
}

// moduleConfig could be in a module specific package
type moduleConfig struct {
	Token string
}

var C config

err := viper.Unmarshal(&C)
if err != nil {
	t.Fatalf("unable to decode into struct, %v", err)
}

viper使用github.com/mitchellh/mapstructure这个包来解析值,所以我们需要将viper反序列化到自定义的结构体变量中时,要使用mapstructure的tags

  • 序列化为字符串
    有时候我们可能需要将viper的所有配置序列化到一个字符串中,而不是将他们写入文件。我们也可以选择自己喜欢的格式序列化AllSettings()返回的所有配置。
    示例如下:
import (
	yaml "gopkg.in/yaml.v2"
	// ...
)

func yamlStringSettings() string {
	c := viper.AllSettings()
	bs, err := yaml.Marshal(c)
	if err != nil {
		log.Fatalf("unable to marshal config to YAML: %v", err)
	}
	return string(bs)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值