Go 每日一库-viper

安装

go get github.com/spf13/viper

功能

viper 是用来管理 go 程序的项目配置,是一个完整的解决方案
它支持:

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

可以将 Viper 视为满足您所有应用程序配置需求的注册表。

实例

package main
  
import (  
   "fmt"  
   "github.com/fsnotify/fsnotify"   "github.com/spf13/viper"   "os"   "strings")  
  
// 设置默认配置信息  
var (  
   v1            = viper.New()  
   defaultConifg = []byte(`  
username: 213  
`)  
)  
  
func init() {  
   parseConfig()  
}  
func main() {  
   //在main函数中添加下列代码  
   v1.WatchConfig()  
   v1.OnConfigChange(func(e fsnotify.Event) {  
      // 配置文件发生变更之后会调用的回调函数  
      fmt.Println("Config file changed:", e.Name)  
   })  
}  
func parseConfig() {  
   // 指定配置文件路径  
   configPath := "./config.yaml"  
   temp1 := strings.Split(configPath, "/")  
   configName := strings.Split(temp1[len(temp1)-1], ".")  
   v1.AddConfigPath(".") // 还可以在工作目录中查找配置  
   v1.SetConfigName(configName[0])  
   v1.SetConfigType(configName[1])  
   if err := v1.ReadInConfig(); err != nil {  
      // 配置文件出错  
      //todo 设置日志输出  
      if _, ok := err.(viper.ConfigFileNotFoundError); ok {  
         // 配置文件未找到错误;如果需要可以忽略  
         os.Create(configPath)  
         //todo 提示生成配置文件  
         //初始化配置文件  
         os.WriteFile(configPath, defaultConifg, 0777)  
         //todo 补充日志创建说明  
      } else {  
         // 配置文件被找到,但产生了另外的错误  
         panic("The configuration file was found, but another error was generated")  
      }  
   }  
  
   // 配置文件找到并成功解析  
}

用法

读取配置文件

读取本地文件

//方法1
viper.SetConfigFile("./config.yaml") // 指定配置文件路径
//方法2(推荐)此方法 viper.ConfigFileNotFoundError 可以正常使用
viper.AddConfigPath("/etc/appname/") 
viper.SetConfigName("config") // 配置文件名称(无扩展名)
viper.SetConfigType("yaml") // 如果配置文件的名称中没有扩展名,则需要配置此项
err := viper.ReadInConfig() // 查找并读取配置文件
if err != nil { // 处理读取配置文件的错误
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}
viper.AddConfigPath("$HOME/.appname")  // 多次调用以添加多个搜索路径

监控并重新读取配置文件

Viper支持在运行时实时读取配置文件的功能。

需要重新启动服务器以使配置生效的日子已经一去不复返了,viper驱动的应用程序可以在运行时读取配置文件的更新,而不会错过任何消息。

只需告诉viper实例watchConfig。可选地,你可以为Viper提供一个回调函数,以便在每次发生更改时运行。

确保在调用WatchConfig()之前添加了所有的配置路径。

viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
  // 配置文件发生变更之后会调用的回调函数
fmt.Println("Config file changed:", e.Name)
})

写入配置文件

从配置文件中读取配置文件是有用的,但是有时你想要存储在运行时所做的所有修改。为此,可以使用下面一组命令,每个命令都有自己的用途:

  • WriteConfig - 将当前的viper配置写入预定义的路径并覆盖(如果存在的话)。如果没有预定义的路径,则报错。
  • SafeWriteConfig - 将当前的viper配置写入预定义的路径。如果没有预定义的路径,则报错。如果存在,将不会覆盖当前的配置文件。
  • WriteConfigAs - 将当前的viper配置写入给定的文件路径。将覆盖给定的文件(如果它存在的话)。
  • SafeWriteConfigAs - 将当前的viper配置写入给定的文件路径。不会覆盖给定的文件(如果它存在的话)。

根据经验,标记为safe的所有方法都不会覆盖任何文件,而是直接创建(如果不存在),而默认行为是创建或截断。

一个小示例:

package main

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

func init() {
	parseConfig()
}
func main() {
	//在main函数中添加下列代码
	viper.WatchConfig()
	viper.OnConfigChange(func(e fsnotify.Event) {
		// 配置文件发生变更之后会调用的回调函数
		fmt.Println("Config file changed:", e.Name)
	})
}
func parseConfig() {
	//设定默认值
	viper.SetDefault("username", "admin")
	// 指定配置文件路径
	configPath := "./config.yaml"
	temp1 := strings.Split(configPath, "/")
	configName := strings.Split(temp1[len(temp1)-1], ".")
	viper.AddConfigPath(".") // 还可以在工作目录中查找配置
	viper.SetConfigName(configName[0])
	viper.SetConfigType(configName[1])
	if err := viper.ReadInConfig(); err != nil {
		// 配置文件出错
		//todo 设置日志输出
		if _, ok := err.(viper.ConfigFileNotFoundError); ok {
			// 配置文件未找到错误;如果需要可以忽略
			viper.SafeWriteConfigAs(configPath)
			//todo 补充日志创建说明
		} else {
			// 配置文件被找到,但产生了另外的错误
			panic("The configuration file was found, but another error was generated")
		}
	}

	// 配置文件找到并成功解析
}

可以从生成的 config.yaml 文件中看到设定的 username: admin

从Viper获取值

为了测试此处提供一份配置文件

username
 -

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

  • 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()方法。

例如:

viper.GetString("logfile") // 不区分大小写的设置和获取
if viper.GetBool("verbose") {
    fmt.Println("verbose enabled")
}

访问嵌套的键

访问器方法也接受深度嵌套键的格式化路径。

Viper可以通过传入.分隔的路径来访问嵌套字段:

viper.GetString("datastore.metric.host") // (返回 "127.0.0.1")

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

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

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

提取子树

对于单一应用程序,可能只需要配置文件中的一部分内容,此时便可以提取部分子树,来减少内存占用

subv := viper.Sub("database.metric")

subv现在就代表:一个单独的配置表,其中的内容是 database.metric 中的全部内容

package main

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

func init() {
	parseConfig()
}
func main() {
	//在main函数中添加下列代码
	viper.WatchConfig()
	viper.OnConfigChange(func(e fsnotify.Event) {
		// 配置文件发生变更之后会调用的回调函数
		fmt.Println("Config file changed:", e.Name)
	})
	fmt.Println(viper.Get("host"))
	subv := viper.Sub("database.metric")
	fmt.Println(subv.Get("host"))
}
func parseConfig() {
	//设定默认值
	// 指定配置文件路径
	configPath := "./config.yaml"
	temp1 := strings.Split(configPath, "/")
	configName := strings.Split(temp1[len(temp1)-1], ".")
	viper.AddConfigPath(".") // 还可以在工作目录中查找配置
	viper.SetConfigName(configName[0])
	viper.SetConfigType(configName[1])
	if err := viper.ReadInConfig(); err != nil {
		// 配置文件出错
		//todo 设置日志输出
		if _, ok := err.(viper.ConfigFileNotFoundError); ok {
			// 配置文件未找到错误;如果需要可以忽略
			viper.SafeWriteConfigAs(configPath)
			//todo 补充日志创建说明
		} else {
			// 配置文件被找到,但产生了另外的错误
			panic("The configuration file was found, but another error was generated")
		}
	}

	// 配置文件找到并成功解析
}

反序列化

你还可以选择将所有或特定的值解析到结构体、map等。

有两种方法可以做到这一点:

  • Unmarshal(rawVal interface{}) : error
  • UnmarshalKey(key string, rawVal interface{}) : error

Viper在后台使用github.com/mitchellh/mapstructure来解析值,其默认情况下使用mapstructuretag。

注意 当我们需要将viper读取的配置反序列到我们定义的结构体变量中时,一定要使用mapstructuretag哦!

举个例子:

package main

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

type config struct {
	Host struct {
		Address string `mapstructure:address`
		Port    string `mapstructure:address`
	} `mapstructure:host`
}

func init() {
	parseConfig()
}
func main() {
	//在main函数中添加下列代码
	viper.WatchConfig()
	viper.OnConfigChange(func(e fsnotify.Event) {
		// 配置文件发生变更之后会调用的回调函数
		fmt.Println("Config file changed:", e.Name)
	})
	conf := config{}
	viper.Unmarshal(&conf)
	fmt.Println(conf)
}
func parseConfig() {
	//设定默认值
	// 指定配置文件路径
	configPath := "./config.yaml"
	temp1 := strings.Split(configPath, "/")
	configName := strings.Split(temp1[len(temp1)-1], ".")
	viper.AddConfigPath(".") // 还可以在工作目录中查找配置
	viper.SetConfigName(configName[0])
	viper.SetConfigType(configName[1])
	if err := viper.ReadInConfig(); err != nil {
		// 配置文件出错
		//todo 设置日志输出
		if _, ok := err.(viper.ConfigFileNotFoundError); ok {
			// 配置文件未找到错误;如果需要可以忽略
			viper.SafeWriteConfigAs(configPath)
			//todo 补充日志创建说明
		} else {
			// 配置文件被找到,但产生了另外的错误
			panic("The configuration file was found, but another error was generated")
		}
	}

	// 配置文件找到并成功解析
}

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

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

v.SetDefault("chart::values", map[string]interface{}{
    "ingress": map[string]interface{}{
        "annotations": map[string]interface{}{
            "traef363ik.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.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")

使用多个viper实例

你还可以在应用程序中创建许多不同的viper实例。每个都有自己独特的一组配置和值。每个人都可以从不同的配置文件,key value存储区等读取数据。每个都可以从不同的配置文件、键值存储等中读取。viper包支持的所有功能都被镜像为viper实例的方法。

例如:

x := viper.New()
y := viper.New()

x.SetDefault("ContentDir", "content")
y.SetDefault("ContentDir", "foobar")

//...

当使用多个viper实例时,由用户来管理不同的viper实例。

注意事项

同名不同类文件的识别问题

当你使用如下方式读取配置时,viper会从./conf目录下查找任何以config为文件名的配置文件,如果同时存在./conf/config.json./conf/config.yaml两个配置文件的话,viper会根据而ext的顺序进行文件读取

viper.SetConfigName("config")
viper.AddConfigPath("./conf")

viper.SetConfigType("yaml")无法实现预期效果

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 ""
}

猜你喜欢

转载自blog.csdn.net/qq_40790680/article/details/128971218