【Golang教程】Go语言配置管理库—Viper

零 Golang教程评论108字数 11782阅读39分16秒阅读模式

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

一、什么是Viper

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

二、什么使用Viper

1.支持多种方式设置

  • 默认值
  • 通过读取 JSON、TOML、YAML、HCL、envfile 和 Java 属性配置文件
  • 实时监听并重新读取配置文件
  • 从环境变量中读取
  • 从远程配置系统(etcd 或 Consul)读取,并监听修改
  • 从命令行的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监听配置的变动,也可以选择在变动之后触发对应的回调函数文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15789.html

go

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

从io.Reader中读取

Viper 预定义了许多配置源,例如文件、环境 变量、标志和远程 K/V 存储,除此此外,也可以自己配置源并将其提供给 Viper。文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15789.html

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

注册和使用别名

别名允许多个键引用单个值文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15789.html

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个函数可以提供对环境变量的支持:文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15789.html

  • 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方法,实例如下:文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15789.html

go

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

也可以绑定一组现有的pflags (pflag.FlagSet)文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15789.html

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,比如:文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15789.html

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,以下是一个如何实现这个接口的简单例子:文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15789.html

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对其进行绑定文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15789.html

go

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

FlagValueSet表示一组flags,以下是一个如何实现这个接口的简单例子:文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15789.html

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.host 和 datastore.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-items和 item-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实例时,用户需要自行跟踪这些不同的实例。

零
  • 转载请务必保留本文链接:https://www.0s52.com/bcjc/golangjc/15789.html
    本社区资源仅供用于学习和交流,请勿用于商业用途
    未经允许不得进行转载/复制/分享

发表评论