Contents

go第三方库-github.com.spf13.viper

godoc

本系列为作者跟着viper官网学习做的笔记

另外相关阅读还可以查看李文周(qimi)的博客

安装

1
go get github.com/spf13/viper

注意: Viper 使用Go Modules来管理依赖项。

什么是Viper?

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

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

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

为什么是Viper?

在构建现代应用程序时,您不必担心配置文件格式;您想专注于构建出色的软件。Viper 是来帮助解决这个问题的。

Viper 为您执行以下操作:

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

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

  • 显式调用Set
  • flag
  • env
  • config
  • 键/值存储
  • default

重要提示: Viper 配置键不区分大小写。正在进行关于将其设为可选的讨论。

将值存入 Viper

建立默认值

一个好的配置系统将支持默认值。密钥不需要默认值,但在尚未通过配置文件、环境变量、远程配置或flag设置密钥的情况下很有用。

例子:

1
2
3
viper.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})

读取配置文件

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

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

1
2
3
4
5
6
7
8
9
viper.SetConfigName("config") // name of config file (without extension)
viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name
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: %w", err))
}

您可以处理找不到配置文件的特定情况,如下所示:

1
2
3
4
5
6
7
8
9
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

注意 [从 1.6 开始]:您也可以有一个没有扩展名的文件,并以编程方式指定格式。对于那些位于用户Home中没有任何扩展名的配置文件.bashrc

编写配置文件

从配置文件中读取很有用,但有时您希望存储在运行时所做的所有修改。为此,可以使用一堆命令,每个命令都有自己的用途:

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

根据经验,标有安全的所有文件都不会覆盖任何文件,如果不存在则创建,而默认行为是创建或截断。

一个小例子部分:

1
2
3
4
5
viper.WriteConfig() // writes current config to predefined path set by 'viper.AddConfigPath()' and 'viper.SetConfigName'
viper.SafeWriteConfig()
viper.WriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.config") // will error since it has already been written
viper.SafeWriteConfigAs("/path/to/my/.other_config")

查看和重新读取配置文件

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

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

只需告诉 viper 实例 watchConfig。或者,您可以为 Viper 提供一个函数,以便在每次发生更改时运行。

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

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

从 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
21
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"

设置覆盖

这些可能来自命令行flag,也可能来自您自己的应用程序逻辑。

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

注册和使用别名

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

1
2
3
4
5
6
7
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 完全支持环境变量。这使开箱即用的 12 因子应用成为可能。有五种方法可以帮助使用 ENV:

  • AutomaticEnv()
  • BindEnv(string…) : error
  • SetEnvPrefix(string)
  • SetEnvKeyReplacer(string…) *strings.Replacer
  • AllowEmptyEnv(bool)

使用 ENV 变量时,重要的是要认识到 Viper 将 ENV 变量视为区分大小写。

Viper 提供了一种机制来尝试确保 ENV 变量是唯一的。通过使用SetEnvPrefix,您可以告诉 Viper 在读取环境变量时使用前缀。BindEnv和AutomaticEnv两者都将使用此前缀。

BindEnv接受一个或多个参数。第一个参数是键名,其余的是要绑定到该键的环境变量的名称。如果提供了多个,它们将按照指定的顺序优先。环境变量的名称区分大小写。如果未提供 ENV 变量名称,那么 Viper 将自动假定 ENV 变量与以下格式匹配:前缀 + “_” + 全部大写的键名。当您显式提供 ENV 变量名称(第二个参数)时,它不会自动添加前缀。例如,如果第二个参数是“id”,Viper 将查找 ENV 变量“ID”。

使用 ENV 变量时要认识到的一件重要事情是每次访问时都会读取该值。BindEnv调用时,Viper 不固定该值。

AutomaticEnv是一个强大的帮手,尤其是与 SetEnvPrefix一起使用时。 在任何时候viper.Get调用时,Viper 将发出请求时检查环境变量。它将应用以下规则。它将检查名称与键的大写形式匹配环境变量,如果设置了EnvPrefix还会匹配是否有该前缀。

SetEnvKeyReplacer允许您使用strings.Replacer对象在一定程度上重写 Env 键。-如果您想在Get()调用中使用-或某些东西 ,但希望您的环境变量使用_分隔符,这很有用。可以在viper_test.go中找到使用它的示例。

或者,您可以工厂函数NewWithOptions使用EnvKeyReplacer。与SetEnvKeyReplacer不同,它接受一个StringReplacer允许您编写自定义字符串替换逻辑的接口。

默认情况下,空环境变量被视为未设置,并将回退到下一个配置源。要将空环境变量视为已设置,请使用该AllowEmptyEnv方法。

环境示例

1
2
3
4
5
6
SetEnvPrefix("spf") // will be uppercased automatically
BindEnv("id")

os.Setenv("SPF_ID", "13") // typically done outside of the app

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

使用标志

Viper 具有绑定flag的能力。具体来说,Viper 支持Pflags 在Cobra库中使用。

像BindEnv,在调用绑定方法时不会设置该值,而是在访问它时设置该值。这意味着您可以尽可能早地绑定,即使在 init()函数中也是如此。

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

例子:

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

您还可以绑定现有的一组 pflags (pflag.FlagSet):

例子:

1
2
3
4
5
6
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包的其他包。pflag 包可以通过导入这些标志来处理为标志包定义的标志。这是通过调用一个名为 AddGoFlagSet() 的 pflag 包提供的便利函数来完成的。

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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 接口来绑定其他flag系统,如果你不使用Pflags.

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

1
2
3
4
5
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" }

一旦你的标志实现了这个接口,你可以简单地告诉 Viper 绑定它:

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

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

1
2
3
4
5
6
7
8
9
type myFlagSet struct {
	flags []myFlag
}

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

一旦你的标志集实现了这个接口,你可以简单地告诉 Viper 绑定它:

1
2
3
4
fSet := myFlagSet{
	flags: []myFlag{myFlag{}, myFlag{}},
}
viper.BindFlagValues("my-flags", fSet)

远程键/值存储支持

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

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

Viper 将读取从 Key/Value 存储(如 etcd 或 Consul)中的路径检索到的配置字符串(如 JSON、TOML、YAML、HCL 或 envfile)。这些值优先于默认值,但会被从磁盘、标志或环境变量中检索到的配置值覆盖。

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

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

crypt有一个命令行助手,您可以使用它来将配置放入您的 K/V 存储中。crypt默认为http://127.0.0.1:4001上的 etcd 。

1
2
$ go get github.com/bketelsen/crypt/bin/crypt
$ crypt set -plaintext /config/hugo.json /Users/hugo/settings/config.json

确认您的值已设置:

1
$ crypt get -plaintext /config/hugo.json

crypt有关如何设置加密值或如何使用 Consul 的示例,请参阅文档。

远程键/值存储示例 - 未加密

etcd

1
2
3
viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
err := viper.ReadRemoteConfig()

etcd3

1
2
3
viper.AddRemoteProvider("etcd3", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
err := viper.ReadRemoteConfig()

Consul

您需要使用包含所需配置的 JSON 值将密钥设置为 Consul 键/值存储。例如,创建一个带有值的Consul键/值存储键MY_CONSUL_KEY:

1
2
3
4
{
    "port": 8080,
    "hostname": "myhostname.com"
}
1
2
3
4
5
6
viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY")
viper.SetConfigType("json") // Need to explicitly set this to json
err := viper.ReadRemoteConfig()

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

Firestore

1
2
3
viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document")
viper.SetConfigType("json") // Config's format: "json", "toml", "yaml", "yml"
err := viper.ReadRemoteConfig()

当然,您也可以使用SecureRemoteProvider

远程键/值存储示例 - 加密

1
2
3
viper.AddSecureRemoteProvider("etcd","http://127.0.0.1:4001","/config/hugo.json","/etc/secrets/mykeyring.gpg")
viper.SetConfigType("json") // because there is no file extension in a stream of bytes,  supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
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
// alternatively, you can create a new viper instance.
var runtime_viper = viper.New()

runtime_viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/hugo.yml")
runtime_viper.SetConfigType("yaml") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"

// read from remote config the first time.
err := runtime_viper.ReadRemoteConfig()

// unmarshal config
runtime_viper.Unmarshal(&runtime_conf)

// open a goroutine to watch remote changes forever
go func(){
	for {
		time.Sleep(time.Second * 5) // delay after each request

		// currently, only tested with etcd support
		err := runtime_viper.WatchRemoteConfig()
		if err != nil {
			log.Errorf("unable to read remote config: %v", err)
			continue
		}

		// unmarshal new config into our runtime config struct. you can also use channel
		// to implement a signal to notify the system of the changes
		runtime_viper.Unmarshal(&runtime_conf)
	}
}()

从 Viper 获取值

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

例子:

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

访问嵌套键

访问器方法还接受深度嵌套键的格式化路径。例如,如果加载了以下 JSON 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
    "host": {
        "address": "localhost",
        "port": 5799
    },
    "datastore": {
        "metric": {
            "host": "127.0.0.1",
            "port": 3099
        },
        "warehouse": {
            "host": "198.0.0.1",
            "port": 2112
        }
    }
}

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

1
GetString("datastore.metric.host") // (returns "127.0.0.1")

这符合上面建立的优先规则;对路径的搜索将通过剩余的配置注册表进行级联,直到找到为止。

例如,给定这个配置文件,datastore.metric.host和 datastore.metric.port都已经定义(并且可能被覆盖)。如果另外 datastore.metric.protocol在默认值中定义,Viper 也会找到它。

但是,如果datastore.metric被立即值覆盖(通过标志、环境变量、Set()方法……),则所有datastore.metric的子键都变为未定义,它们被更高优先级的配置级别“遮蔽”。

Viper 可以通过在路径中使用数字来访问数组索引。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
    "host": {
        "address": "localhost",
        "ports": [
            5799,
            6029
        ]
    },
    "datastore": {
        "metric": {
            "host": "127.0.0.1",
            "port": 3099
        },
        "warehouse": {
            "host": "198.0.0.1",
            "port": 2112
        }
    }
}
1
GetInt("host.ports.1") // returns 6029

最后,如果存在与分隔键路径匹配的键,则将返回其值。例如

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
    "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
        }
    }
}
1
GetString("datastore.metric.host") // returns "0.0.0.0"

提取子树

在开发可重用模块时,提取配置的子集并将其传递给模块通常很有用。这样,可以使用不同的配置多次实例化模块。

例如,一个应用程序可能会出于不同目的使用多个不同的缓存存储:

1
2
3
4
5
6
7
cache:
  cache1:
    max-items: 100
    item-size: 64
  cache2:
    max-items: 200
    item-size: 80

我们可以将缓存名称传递给模块(例如NewCache(“cache1”)),但它需要奇怪的连接来访问配置键,并且与全局配置的分离度较低。

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

1
2
3
4
5
6
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键:

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

生成的代码很容易测试,因为它与主配置结构分离,并且更容易重用(出于同样的原因)。

解组

您还可以选择将所有或特定值解组到结构、map等。

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

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

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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)
}

如果要解组键本身包含点(默认键分隔符)的配置,则必须更改分隔符:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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 还支持解组到嵌入式结构:

 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
/*
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来解组默认使用mapstructure标签的值。

解码自定义格式

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

这已经在 Viper 中使用 mapstructure decode hooks 提供了。

编组到字符串

您可能需要将 viper 中保存的所有设置编组为字符串,而不是将它们写入文件。您可以通过调用AllSettings()返回您最喜欢的格式的编组器.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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 or Vipers?

Viper 开箱即用。开始使用 Viper 无需配置或初始化。由于大多数应用程序都希望使用单个中央存储库进行配置,因此 viper 包提供了这一点。它类似于单例。

在上面的所有示例中,他们都演示了在其单例样式方法中使用 viper。

Working with multiple vipers

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

例子:

1
2
3
4
5
6
7
x := viper.New()
y := viper.New()

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

//...

当使用多个Viper时,由用户负责跟踪不同的Viper。

问答

为什么叫“Viper”蝰蛇?

答:Viper 旨在成为 Cobra的伴侣。虽然两者都可以完全独立运行,但它们共同构成了强大的组合,可以满足您的大部分应用程序基础需求。

为什么叫“cobra”眼镜蛇?

指挥官有更好的名字吗?

Viper 是否支持区分大小写的键?

tl;博士:不。

Viper 合并了来自各种来源的配置,其中许多要么不区分大小写,要么使用与其他来源不同的大小写(例如 env vars)。为了在使用多个来源时提供最佳体验,已决定使所有键不区分大小写。

已经有几次尝试实现区分大小写,但不幸的是,这并不是那么微不足道。我们可能会尝试在Viper v2中实现它,但尽管有最初的噪音,但似乎并没有太多要求。

您可以通过填写此反馈表来投票支持区分大小写:https ://forms.gle/R6faU74qPRPAzchZ9

同时读写viper是否安全?

不,您需要自己同步对 viper 的访问(例如通过使用sync包)。并发读取和写入可能会导致恐慌。

 |