Go语言配置管理库---Viper

项目中使用到了Viper配置环境,本文主要想对Viper的用法进行整理,对Viper仓库的README文件进行了翻译用于记录学习。

一、什么是Viper

Viper是一个用于go语言项目配置的库,它可以简化项目的配置过程,具有灵活且丰富的特性。

二、什么使用Viper

1.支持多种方式设置

  • 默认值
  • 通过读取 JSON、TOML、YAML、HCL、envfile Java 属性配置文件
  • 实时监听并重新读取配置文件
  • 从环境变量中读取
  • 从远程配置系统(etcdConsul)读取,并监听修改
  • 从命令行的flag中读取
  • buffer中读取

2.丰富的特性

  • 可以从JSONTOMLYAMLHCLINIenvfileJava properties格式中进行查找、加载以及反序列化
  • 提供一种机制来为不同的配置选项设置默认值。
  • 提供一种机制,用于为通过命令行标志指定的选项设置替代值。
  • 提供别名系统,以便在不破坏现有代码的情况下轻松重命名参数。
  • 轻松区分用户何时提供了与默认相同的命令行或配置文件。

三、Viper安装以及使用

1.安装

js 复制代码
 $ go get github.com/spf13/viper

2.使用

go 复制代码
package main

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

func main() {
    //...具体配置
}

四、Viper配置项

1.配置项优先级

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

1.设置默认值:SetDefault

go 复制代码
//配置项: ContentDir 默认值: content
viper.SetDefault("ContentDir", "content")

2.读取配置文件:

go 复制代码
viper.SetConfigName("config") // 配置文件名称(无扩展名)
viper.SetConfigFile("./config.yaml") // 指定配置文件路径
viper.SetConfigType("yaml") // 如果配置文件的名称中没有扩展名,则需要指定
viper.AddConfigPath("/etc/appname/")   // 查找配置文件所在的路径
viper.AddConfigPath("$HOME/.appname")  // 多次调用可以多次搜索路径
viper.AddConfigPath(".")               // 选择性的在工作目录中查找配置
// 处理找不到配置文件的情况
err := viper.ReadInConfig() 
if err != nil { 
	panic(fmt.Errorf("Fatal error config file: %s \n", err))
}

编写配置文件

go 复制代码
//  将当前 viper 配置写入预定义的路径(如果存在)。如果没有预定义的路径,将报错。如果配置文件已经存在则将覆盖当前配置文件。
viper.WriteConfig() 
//  和WriteConfig类似只是不会覆盖
viper.SafeWriteConfig()
//  
viper.WriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.other_config")

查看和重新读取配置文件

Viper 支持应用程序在运行时实时读取配置文件。 可以通过配置使得Viper监听配置的变动,也可以选择在变动之后触发对应的回调函数

go 复制代码
//监听变化
viper.WatchConfig()
// 变动之后的回调
viper.OnConfigChange(func(e fsnotify.Event) {
	fmt.Println("Config file changed:", e.Name)
})

从io.Reader中读取

Viper 预定义了许多配置源,例如文件、环境 变量、标志和远程 K/V 存储,除此此外,也可以自己配置源并将其提供给 Viper。

go 复制代码
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"

覆盖设置

go 复制代码
viper.Set("Verbose", true)
viper.Set("LogFile", LogFile)
viper.Set("host.port", 5899)   // set subset

注册和使用别名

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

go 复制代码
viper.RegisterAlias("loud", "Verbose")

viper.Set("verbose", true) // 结果和下一行相同
viper.Set("loud", true)   // 结果和前一行相同

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

使用环境变量

Viper完全支持环境变量,它使得12 factor applications应用程序做到开箱即用,通过如下5个函数可以提供对环境变量的支持:

  • AutomaticEnv()
  • BindEnv(string...) : error
  • SetEnvPrefix(string)
  • SetEnvKeyReplacer(string...) *strings.Replacer
  • AllowEmptyEnv(bool) 在使用环境变量时,需要注意Viper对环境变量是区分大小写的 Viper通过一种机制去确保ENV变量是唯一的,SetEnvPrefix可以指定Viper在读取环境变量时的前缀。
    BindEnv需要传入一个或者多个参数,第一个参数是键名,其余参数是该键名所对应的花环境变量,如果其有一个以上,会按照指定的优先顺序处理。如果没有提供ENV变量名,则会去匹配规则为:前缀+ "_" +键名全部大写。如果在第二个参数中显式的提供了ENV变量,则不会去自动的添加前缀例如,如果第二个参数是"id",Viper将查找环境变量"ID"。 在使用ENV变量的时候一个重要的点是,ENV变量的值会在其每次被访问时读取,Viper在调用BindEnv时不会固定ENV变量的值。
    AutomaticEnv 是一个很强大的助手,尤其是与SetEnvPrefix结合使用时,在任何时候当viper.Get请求发出时,Viper会随时对环境变量进行检查。它遵循如下的规则:如果设置了EnvPrefix,会检查一个环境变量的名称是否匹配大写的键和EnvPrefix所设置的前缀。
    SetEnvKeyReplacer允许你使用strings.Replacer对象去重写ENV的键,这在你希望于Call()调用过程中使用分隔符-,但是在环境变量中使用_,这将非常有用。
    与此同时,你也可以使用EnvKeyReplacer以及NewWithOptions工厂函数,不同于SetEnvKeyReplacer,它能接收一个StringReplacer接口允许你去自定义字符串的替换逻辑。
    使用Env的示例如下:
go 复制代码
SetEnvPrefix("spf") // 会被自动变为大小写
BindEnv("id")

os.Setenv("SPF_ID", "13") // 通常在程序之外处理完成

id := Get("id") // 13

使用Flags

Viper有绑定flags的能力,具体的,Viper支持如Cobra 库中使用的pflgs,和BindEnv类似,其值不是在绑定方法被调用的时候被设置的,而是在其被访问的时候。这意味着你可以尽早的对其进行绑定,即使在init()中也能起作用。

如果需要绑定单个flags,可以使用BindPFlag方法,实例如下:

go 复制代码
serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))

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

go 复制代码
pflag.Int("flagname", 1234, "help message for flagname")

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

i := viper.GetInt("flagname") // retrieve values from viper instead of pflag

在Viper中使用pflag,并不影响标准库中flag包的使用,flag包可以通过使用AddGoFlagSet()导入这些 flags 来处理其定义的flags,比如:

go 复制代码
package main

import (
	"flag"
	"github.com/spf13/pflag"
)

func main() {

	// 使用标准库 "flag" 包
	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接口

如果不适用Pflags,Viper提供了两个接口绑定其它的flag系统,FlagValue代表单个的flag,以下是一个如何实现这个接口的简单例子:

go 复制代码
type myFlag struct {}
func (f myFlag) HasChanged() bool { return false }
func (f myFlag) Name() string { return "my-flag-name" }
func (f myFlag) ValueString() string { return "my-flag-value" }
func (f myFlag) ValueType() string { return "string" }

一旦你的flag实现了这样的接口,就可以很方便的通过Viper对其进行绑定

go 复制代码
viper.BindFlagValue("my-flag-name", myFlag{})

FlagValueSet表示一组flags,以下是一个如何实现这个接口的简单例子:

go 复制代码
type myFlagSet struct {
	flags []myFlag
}

func (f myFlagSet) VisitAll(fn func(FlagValue)) {
	for _, flag := range flags {
		fn(flag)
	}
}

一旦你的flag实现了这样的接口,就可以很方便的通过Viper对其进行绑定

go 复制代码
fSet := myFlagSet{
	flags: []myFlag{myFlag{}, myFlag{}},
}
viper.BindFlagValues("my-flags", fSet)

远程Key/Value存储支持

若要在Viper中启动远程支持,需要匿名导入viper/remote

go 复制代码
import _ "github.com/spf13/viper/remote"

Viper将会从类似(ectd或者Consul)的Key/Value存储中路径中检索到配置字符串如(as JSON, TOML, YAML, HCL or envfile) 这些值的优先级高于默认值,但是会被从磁盘、flags和环境变量中检索到的 配置值所覆盖。

Viper支持多主机,如果要使用该种配置,需要使用;进行分隔离,比如: http://127.0.0.1:4001;http://127.0.0.1:4002

Viper使用crypt从K/V存储中检索配置,这意味着你可以通过加密的方式存储配置的值,并且在拥有gpg密钥的情况下自动解密,加密功能是可选的。

你可以同时使用远程和本地配置,也可以单独使用远程配置。
crypt 有一个命令行助手用来将配置存到K/V存储中,crypt默认使用位于http://127.0.0.1:4001 的 etcd

go 复制代码
$ go get github.com/sagikazarmark/crypt/bin/crypt
$ crypt set -plaintext /config/hugo.json /Users/hugo/settings/config.json

确认你的值已经设置:

go 复制代码
$ crypt get -plaintext /config/hugo.json

查阅crypt文档的例子可以了解如何进行加密以及如何使用Consul

远程 K/V存储示例 - 未加密

etcd

go 复制代码
viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json") // 因为字节流中没有扩展名,需要设置,支持的扩展名为:"json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
err := viper.ReadRemoteConfig()

etcd3

go 复制代码
viper.AddRemoteProvider("etcd3", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json") // 因为字节流中没有扩展名,需要设置,支持的扩展名为:"json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
err := viper.ReadRemoteConfig()

Consul

你需要在Consul key/value存储中设置一个key,包含所需配置的JSON值,比如下面所示的MY_CONSUL_KEY

go 复制代码
{
    "port": 8080,
    "hostname": "myhostname.com"
}
go 复制代码
viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY")
viper.SetConfigType("json") // 需要显式设置成格式'json'
err := viper.ReadRemoteConfig()

fmt.Println(viper.Get("port")) // 8080
fmt.Println(viper.Get("hostname")) // myhostname.com

Firestore

go 复制代码
viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document")
viper.SetConfigType("json") // 配置格式: "json", "toml", "yaml", "yml"
err := viper.ReadRemoteConfig()

当然,你也可以使用SecureRemoteProvider

NATS

go 复制代码
viper.AddRemoteProvider("nats", "nats://127.0.0.1:4222", "myapp.config")
viper.SetConfigType("json")
err := viper.ReadRemoteConfig()

远程Key/Value存储示例-加密

go 复制代码
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", "env", "dotenv"
err := viper.ReadRemoteConfig()

监听etcd中的更改-未加密

go 复制代码
// 同样的,你可以创建一个新的viper实例
var runtime_viper = viper.New()

runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml")
runtime_viper.SetConfigType("yaml") // 因为字节流中没有扩展名,需要设置,支持的扩展名为: "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"

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

// 反序列化配置
runtime_viper.Unmarshal(&runtime_conf)

// 开启一个 goroutine 持续监听远端改变
go func(){
	for {
		time.Sleep(time.Second * 5) // 每次请求后的延迟
		// 目前,只测试了etcd支持
		err := runtime_viper.WatchRemoteConfig()
		if err != nil {
			log.Errorf("unable to read remote config: %v", err)
			continue
		}

		// 你也可以使用channel去反序列化新的配置到运行时的配置结构体中 
		// 去实现一个信号去通知系统更改
		runtime_viper.Unmarshal(&runtime_conf)
	}
}()

从Viper中获取值

在Viper中,根据值的类型不同,有很多方法去获取,有以下获取值的的方法:

  • Get(key string) : any
  • 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]any
  • 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]any

有一件重要的事需要被认识到: 当值没有被找到时,每一个函数都会返回一个零值,如果需要检查一个给定的key是否存在,Viper提供了IsSet()方法。

例子:

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

访问嵌套的键

访问器方法也接受深度嵌套键的格式化路径,例如加载如下的JSON文件

go 复制代码
{
    "host": {
        "address": "localhost",
        "port": 5799
    },
    "datastore": {
        "metric": {
            "host": "127.0.0.1",
            "port": 3099
        },
        "warehouse": {
            "host": "198.0.0.1",
            "port": 2112
        }
    }
}

Viper可以通过.分隔符去访问嵌套

go 复制代码
GetString("datastore.metric.host") // (返回 "127.0.0.1")

这遵守上述的优先规则,对于路径的搜索将会依次在配置注册表中寻找,直到找到为止 ,例如有这样一个配置文件,datastore.metric.hostdatastore.metric.port 均已经被定义(并且可能会被覆盖),另外 datastore.metric.protocol 也在默认值中被定义, Viper 也会找到他。

然而,如果datastore.metric 被立即值覆盖(通过flag,环境变量,Set()方法等), 那么datastore.metric的所有子键都将变为未定义状态,它们被高优先级配置级别遮蔽了。

Viper可以通过在路径中使用数字去访问数组的索引值,例如:

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

GetInt("host.ports.1") // returns 6029

最后,如果存在一个和分隔键路径匹配的,其值将会被返回,例如:

go 复制代码
{
    "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"

提取子树

在开发可重用模块时,通常会提取配置的子集并将其传递给模块是很有用的。这样一来,可以多次实例化该模块,使用不同的配置。 例如,一个应用程序可能会为不同的目的使用多个不同的缓存存储:

go 复制代码
cache:
  cache1:
    max-items: 100
    item-size: 64
  cache2:
    max-items: 200
    item-size: 80

我们可以将缓存名称传递给一个模块(例如 NewCache("cache1")),但这将需要奇怪的串联来访问配置键,并且与全局配置更少分离。

因此,我们不这样做,而是将表示配置子集的 Viper 实例传递给构造函数:

go 复制代码
cache1Config := viper.Sub("cache.cache1")
if cache1Config == nil { // Sub returns nil if the key cannot be found
	panic("cache configuration not found")
}

cache1 := NewCache(cache1Config)

注意:始终检查 Sub 的返回值。如果找不到键,它将返回 nil。 在内部,NewCache 函数可以直接访问 max-itemsitem-size

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

由此产生的代码易于测试,因为它与主配置结构解耦,并且更容易重用(出于同样的原因)。

反序列化

ChatGPT

你还可以选择将所有或特定值解码到结构体、映射等。

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

  • Unmarshal(rawVal any) : error
  • UnmarshalKey(key string, rawVal any) : error 例如:
go 复制代码
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)
}

如果你想解码包含点的配置(默认键分隔符)的配置,你需要更改分隔符:

go 复制代码
v := viper.NewWithOptions(viper.KeyDelimiter("::"))

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

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

var C config

v.Unmarshal(&C)

Viper还支持将值解码到嵌套结构中:

go 复制代码
/*
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/go-viper/mapstructure 库,该库默认使用 mapstructure 标签。

解码自定义格式

解码自定义格式 Viper经常被要求添加更多值格式和解码器的功能。例如,将字符(点、逗号、分号等)分隔的字符串解析成切片。

Viper已经支持了这一功能,使用 mapstructure 解码钩子。

了解更多详情,请参阅文章

序列化成字符串

你可能需要将Viper中保存的所有设置序列化为字符串,而不是写入文件。你可以使用你喜欢的格式的序列化器和AllSettings()返回的配置。

go 复制代码
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)
}

使用单个还是多个Viper

Viper自带一个全局实例(单例),方便快速配置。

尽管它使配置设置变得容易,但通常不建议使用,因为它会使测试变得更加困难,并可能导致意外行为。

最佳实践是初始化一个Viper实例,并在必要时传递该实例。

全局实例可能在未来被弃用。有关更多详情,请参阅[#1855].(github.com/spf13/viper...)

使用多个Viper

您还可以创建许多不同的Viper实例来用于您的应用程序。每个实例将具有其自己独特的配置和值。每个实例都可以从不同的配置文件、键值存储等读取。Viper包支持的所有函数都被作为Viper实例的方法进行了镜像。

示例:

go 复制代码
x := viper.New()
y := viper.New()

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

在使用多个Viper实例时,用户需要自行跟踪这些不同的实例。

相关推荐
煎鱼eddycjy3 小时前
新提案:由迭代器启发的 Go 错误函数处理
go
煎鱼eddycjy4 小时前
Go 语言十五周年!权力交接、回顾与展望
go
不爱说话郭德纲20 小时前
聚焦 Go 语言框架,探索创新实践过程
go·编程语言
0x派大星2 天前
【Golang】——Gin 框架中的 API 请求处理与 JSON 数据绑定
开发语言·后端·golang·go·json·gin
IT书架2 天前
golang高频面试真题
面试·go
郝同学的测开笔记2 天前
云原生探索系列(十四):Go 语言panic、defer以及recover函数
后端·云原生·go
秋落风声3 天前
【滑动窗口入门篇】
java·算法·leetcode·go·哈希表
0x派大星5 天前
【Golang】——Gin 框架中的模板渲染详解
开发语言·后端·golang·go·gin
0x派大星5 天前
【Golang】——Gin 框架中的表单处理与数据绑定
开发语言·后端·golang·go·gin
三里清风_6 天前
如何使用Casbin设计后台权限管理系统
golang·go·casbin