Go 使用 Viper 进行配置管理

发布于:
更新于:

Go 使用 Viper 进行配置管理

Go configuration with fangs!

许多 Go 项目都使用了 Viper,包括:

Install

go get github.com/spf13/viper

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

What is Viper?

Viper 为 Go 应用程序(包括 12-Factor apps)提供了一整套配置解决方案。它被设计用于在应用程序内部工作,能够处理所有类型的配置需求和格式。它支持:

  • 设置默认值
  • 从 JSON、TOML、YAML、HCL、envfile 和 Java properties 配置文件中读取
  • 监控并实时重新读取配置文件(可选)
  • 从环境变量中读取
  • 从远程配置系统(etcd 或 Consul)读取并监控变更
  • 从命令行标志(flags)读取
  • 从 buffer 中读取
  • 设置显式值

可以把 Viper 看作你所有应用程序配置需求的注册中心。

Why Viper?

当构建一个现代应用时,你不想花时间担心配置文件的格式;你只想专注于打造出色的软件。Viper 就是为此而生。

Viper 为你完成:

  1. 查找、加载并将配置文件(JSON、TOML、YAML、HCL、INI、envfile 或 Java properties 格式)反序列化为结构。
  2. 提供一种机制为不同的配置选项设置默认值。
  3. 提供一种机制,通过命令行标志设置覆盖值(override)。
  4. 提供别名系统,让你无需破坏现有代码就能轻松重命名参数。
  5. 便于区分用户是否通过命令行或配置文件提供了与默认值相同的值。

Viper 的优先级顺序如下。每一项的优先级都高于下一项:

  • 显式调用 Set
  • flag
  • env
  • config
  • key/value store
  • default

重要: Viper 的配置键不区分大小写。
目前围绕是否使其可选还在讨论中。

Putting Values into Viper

Establishing Defaults

一个好的配置系统会支持默认值。某个 key 不一定非要有默认值,但如果它没有被配置文件、环境变量、远程配置或 flag 设置,那么默认值就很有用。

示例:

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

Reading Config Files

Viper 需要进行一些最小配置才能知道到哪里去查找配置文件。Viper 支持 JSON、TOML、YAML、HCL、INI、envfile 和 Java Properties 文件。Viper 可以搜索多个路径,但目前单个 Viper 实例只能支持一个配置文件。Viper 不会对配置搜索路径提供默认值,而是将默认决策留给应用程序去做。

下面是一个示例,展示如何使用 Viper 搜索并读取配置文件。示例中的路径都不是必须的,但至少需要提供一个期望找到配置文件的路径:

viper.SetConfigName("config") // 配置文件名称(不带扩展名)
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: %w", err))
}

可以这样处理找不到配置文件的特定情况:

if err := viper.ReadInConfig(); err != nil {
	if _, ok := err.(viper.ConfigFileNotFoundError); ok {
		// 未找到配置文件;若需要可忽略错误
	} else {
		// 尽管找到了配置文件,但出现了其他错误
	}
}

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

注意 [从 1.6 开始]:你也可以使用没有扩展名的文件,并通过代码来指定文件格式。对那些位于用户主目录且没有扩展名(如 .bashrc)的配置文件适用。

Writing Config Files

从配置文件读取很有用,但是有时你希望在运行时存储所有修改。针对这一需求,有一些可用的命令,每个都有其特定目的:

  • WriteConfig - 将当前 viper 配置写入预定义路径(如果存在)。若不存在预定义路径则报错;如果配置文件已存在则覆盖它。
  • SafeWriteConfig - 将当前 viper 配置写入预定义路径。若不存在预定义路径则报错;如果配置文件已存在则不覆盖它。
  • WriteConfigAs - 将当前 viper 配置写入给定文件路径。如果文件已存在则覆盖它。
  • SafeWriteConfigAs - 将当前 viper 配置写入给定文件路径。如果文件已存在则不覆盖它。

一般来说,带有 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")

Watching and re-reading config files

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

告别了需要重启服务器才能让配置生效的时代,基于 viper 的应用在运行时就能读取到对配置文件的更新,且不会错过任何变更。

只需要告诉 Viper 实例去 watchConfig。可选地,你可以提供一个函数,在检测到变更时让 Viper 执行。

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

viper.OnConfigChange(func(e fsnotify.Event) {
	fmt.Println("配置文件发生变化:", e.Name)
})
viper.WatchConfig()

Reading Config from io.Reader

Viper 预先定义了许多配置源,比如文件、环境变量、标志和远程 K/V 存储,但你不必局限于这些。你也可以自己实现所需的配置源,然后将其输入到 viper 中。

viper.SetConfigType("yaml") // 或 viper.SetConfigType("YAML")

// 将此配置以任意方式引入程序
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") // 将返回 "steve"

Setting Overrides

这些值可能来自命令行标志,也可能由应用程序逻辑本身设定。

viper.Set("Verbose", true)
viper.Set("LogFile", LogFile)
viper.Set("host.port", 5899)   // 设置子集

Registering and Using Aliases

别名允许通过多个键来引用同一个值。

viper.RegisterAlias("loud", "Verbose")

viper.Set("verbose", true) // 与下一行效果相同
viper.Set("loud", true)   // 与上一行效果相同

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

Working with Environment Variables

Viper 对环境变量提供了完整的支持。这让 12-factor 应用可以直接开箱即用。以下是五种与环境变量配合使用的方法:

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

在使用环境变量时,重要的一点是要认识到 Viper 将环境变量视为区分大小写(case sensitive)。

Viper 提供了一种机制来尽量保证环境变量名唯一。通过使用 SetEnvPrefix,可以告诉 Viper 在读取环境变量时使用一个前缀。BindEnvAutomaticEnv 都会使用这个前缀。

BindEnv 接受一个或多个参数。第一个参数是键名,后续参数是要绑定到这个键的环境变量名。如果提供了多个,则按照指定顺序决定优先级。环境变量名是区分大小写的。如果没有提供环境变量名,则 Viper 会自动假设环境变量遵循以下格式:prefix + ”_” + 键名(全部大写)。当你显式地提供了环境变量名(第二个参数)时,不会自动添加该前缀。例如,如果第二个参数是 “id”,则 Viper 会寻找环境变量 “ID”。

在使用环境变量时,要注意的另一个重点在于,每次访问时 Viper 都会重新读取其值。Viper 在调用 BindEnv 时并不会固定环境变量的值。

AutomaticEnv 是一个功能强大的辅助方法,尤其是与 SetEnvPrefix 结合使用时。当调用它时,Viper 在每次执行 viper.Get 请求时都会检查相应的环境变量。它会遵循如下规则:查找与键名对应的(全部大写的)环境变量,并在设置了 EnvPrefix 的情况下在环境变量名前加上这个前缀。

SetEnvKeyReplacer 允许你使用一个 strings.Replacer 对象在一定程度上重写环境变量的键名。如果你想在调用 Get() 时使用 “-” 或其它字符,但又希望环境变量使用 “_” 来分隔,这会很有用。可在 viper_test.go 中找到其用法示例。

或者,你可以与 NewWithOptions 工厂函数搭配使用 EnvKeyReplacer。与 SetEnvKeyReplacer 不同的是,它接收一个 StringReplacer 接口,使你能编写自定义字符串替换逻辑。

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

Env example

SetEnvPrefix("spf") // 会被自动转为大写
BindEnv("id")

os.Setenv("SPF_ID", "13") // 通常在应用外部进行设置

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

Working with Flags

Viper 能够与命令行标志(flags)进行绑定。具体来说,Viper 支持在 Cobra 库中使用的 Pflags

BindEnv 类似,值并不会在绑定方法被调用时设置,而是在访问时才设置。这表示你可以在任意早的时机进行绑定,即便是在 init() 函数中也可以。

对于单个标志,可以通过 BindPFlag() 来实现绑定。

示例:

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

你也可以绑定一组已有的 pflags(pflag.FlagSet):

示例:

pflag.Int("flagname", 1234, "help message for flagname")

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

i := viper.GetInt("flagname") // 从 viper 而非 pflag 获取值

在 Viper 中使用 pflag 并不排斥使用标准库的 flag 包中的其它包。通过从 flag 包导入标志,pflag 包可以处理由 flag 包定义的标志。这是通过 pflag 包提供的一个便捷函数 AddGoFlagSet() 实现的。

示例:

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") // 从 viper 中获取值

	// ...
}

Flag interfaces

如果你不使用 Pflags,Viper 提供了两个 Go 接口来绑定其它标志系统。

FlagValue 表示一个单独的标志。下面是一个如何实现该接口的非常简单的示例:

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 来绑定它:

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

FlagValueSet 表示一组标志。下面是一个如何实现该接口的非常简单的示例:

type myFlagSet struct {
	flags []myFlag
}

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

一旦你的标志集合实现了这个接口,你就可以让 Viper 来绑定它:

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

Remote Key/Value Store Support

要在 Viper 中启用远程支持,需要以空导入的方式导入 viper/remote 包:

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

Viper 会从 Key/Value 存储(如 etcd 或 Consul)中的路径读取字符串形式的配置(可以是 JSON、TOML、YAML、HCL 或 envfile),然后进行解析。这些值比默认值具有更高的优先级,但会被磁盘配置、标志或环境变量获取的配置值覆盖。

Viper 支持多个主机。要使用它,可以将多个端点用 ; 分隔,例如 http://127.0.0.1:4001;http://127.0.0.1:4002

Viper 使用 crypt 从 K/V 存储中检索配置,这意味着如果你拥有正确的 gpg keyring,你可以把配置值加密存储,然后自动解密。加密是可选的。

你可以将远程配置与本地配置同时使用,也可以单独使用。

crypt 提供了一个命令行辅助工具,你可以用它来把配置放进 K/V 存储里。crypt 默认使用 http://127.0.0.1:4001 的 etcd。

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

确认你的值已经被设置:

$ crypt get -plaintext /config/hugo.json

查看 crypt 文档获取有关如何设置加密值或如何使用 Consul 的示例。

Remote Key/Value Store Example - Unencrypted

etcd

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

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 存储中设置一个带有 JSON 值的键,以包含所需配置。比如,在 Consul key/value 存储中创建一个键为 MY_CONSUL_KEY,值为:

{
    "port": 8080,
    "hostname": "myhostname.com"
}
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

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

当然,你也可以使用 SecureRemoteProvider

NATS

viper.AddRemoteProvider("nats", "nats://127.0.0.1:4222", "myapp.config")
viper.SetConfigType("json")
err := viper.ReadRemoteConfig()

Remote Key/Value Store Example - Encrypted

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()

Watching Changes in etcd - Unencrypted

// 或者,你可以创建一个新的 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("无法读取远程配置: %v", err)
			continue
		}

		// 将新配置解组到我们的运行时配置结构中。你也可以使用 channel
		// 来实现一个通知系统发生更改的信号
		runtime_viper.Unmarshal(&runtime_conf)
	}
}()

Getting Values From 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

需要注意的一点是,如果没有找到相应的键,每个 Get 函数都会返回一个零值。为了检查给定键是否存在,可以使用 IsSet() 方法。

如果键已经设置,但无法解析为请求的类型,也会返回零值。

示例:

viper.GetString("logfile") // 大小写不敏感的 Set 和 Get
if viper.GetBool("verbose") {
	fmt.Println("verbose enabled")
}

Accessing nested keys

这些访问器方法同样接受格式化的路径来访问深层嵌套的键。
例如,如果加载了以下 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") // (返回 "127.0.0.1")

这遵循前面提到的优先级规则;搜索的路径会在其后的配置注册表中级联查找,直到找到为止。

例如,给定这个配置文件,datastore.metric.hostdatastore.metric.port 都已经定义了(并且可能被覆盖)。如果还在默认值里定义了 datastore.metric.protocol,Viper 同样可以找到它。

但是,如果 datastore.metric 被某个更高优先级的配置层(比如命令行标志、环境变量或通过 Set() 方法设置等)直接覆盖成一个即时值,那么它的所有子键都会变为未定义状态,因为它们被更高优先级的配置层“遮蔽”了。

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

{
    "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") // 返回 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") // 返回 "0.0.0.0"

Extracting a sub-tree

在开发可重用模块时,通常需要提取配置中的一个子集并将其传给模块。这样一来,同一个模块就可以用不同的配置实例化多次。

举例来说,一个应用可能会为了不同的目标使用多个不同的缓存存储:

cache:
  cache1:
    max-items: 100
    item-size: 64
  cache2:
    max-items: 200
    item-size: 80

我们可以直接把缓存名传给模块(比如 NewCache("cache1")),但那样就需要使用一些不太优雅的字符串拼接来访问配置键,而且和全局配置的耦合也较紧。

所以,我们可以改为将代表该子配置的 Viper 实例传给构造函数:

cache1Config := viper.Sub("cache.cache1")
if cache1Config == nil { // 如果没找到对应的键,Sub 会返回 nil
	panic("cache configuration not found")
}

cache1 := NewCache(cache1Config)

注意: 一定要检查 Sub 的返回值。如果键不存在,会返回 nil

NewCache 函数内部,就可以直接访问 max-itemsitem-size

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

这样不仅易于测试,因为与主配置结构解耦,也更易重用(出于同样的原因)。

Unmarshaling

你也可以选择将所有或特定的配置值解组(Unmarshal)到一个结构体、映射等。

有两种方法可以做到:

  • Unmarshal(rawVal any) : error
  • UnmarshalKey(key string, rawVal any) : 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("无法解码到结构体: %v", err)
}

如果你想解组的配置键本身包含点(即默认的键分隔符),你需要修改分隔符:

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 同样支持解组到嵌入式结构体中:

/*
示例配置:

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

		moduleConfig `mapstructure:",squash"`
	}
}

// moduleConfig 可能在一个模块专用的包里
type moduleConfig struct {
	Token string
}

var C config

err := viper.Unmarshal(&C)
if err != nil {
	t.Fatalf("无法解码到结构体: %v", err)
}

Viper 在内部使用了 github.com/go-viper/mapstructure 来解组值,该库默认使用 mapstructure 标签。

Decoding custom formats

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

其实,这已经可以通过 Viper 的 mapstructure decode hooks 实现。

可在这篇博客了解更多细节。

Marshalling to string

有时你需要将 Viper 中的所有设置序列化(marshal)成字符串,而不是写到文件里。
你可以结合 AllSettings() 返回的配置,使用自己喜欢的格式进行序列化。

import (
	yaml "gopkg.in/yaml.v2"
	// ...
)

func yamlStringSettings() string {
	c := viper.AllSettings()
	bs, err := yaml.Marshal(c)
	if err != nil {
		log.Fatalf("无法将配置序列化为 YAML: %v", err)
	}
	return string(bs)
}

Viper 还是 Vipers?

Viper 自带了一个全局实例(singleton)。

虽然它让配置变得容易,通常并不推荐使用,因为这会使测试变得更加困难,并且可能导致意外行为。

最佳实践是在需要时初始化一个 Viper 实例并进行传递。

将来 可能 会弃用全局实例。更多详情请参见 #1855

使用多个 viper

你也可以在应用程序中创建多个不同的 viper。每个 viper 都会有自己独立的一套配置和数值。它们可以读取不同的配置文件、键值存储等。viper 包支持的所有函数都可以作为方法在 viper 实例上调用。

示例:

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

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

//...

当使用多个 viper 时,需要由用户自己来管理不同的 viper。

Q & A

为什么叫 “Viper”?

答:Viper 被设计为 Cobra 的一个 搭档。两者都可以独立运行,但同时使用时,它们能为应用的基础需求提供强大的组合能力。

为什么叫 “Cobra”?

有没有一个更好的名字来代表一个 指挥官

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

简短回答(tl;dr): 不支持。

Viper 会从各种不同的来源合并配置,这些来源中很多要么不区分大小写,要么使用与其他来源不一致的大小写方式(例如环境变量)。 为了在使用多种来源时提供最好的体验,我们决定让所有键都不区分大小写。

曾经有过几次尝试去实现区分大小写,但并没有那么简单。我们或许会在 Viper v2 中努力实现它,但尽管最初呼声很高,目前似乎并没有那么多人真正需要它。

如果你希望支持区分大小写,可以通过以下反馈表投票: https://forms.gle/R6faU74qPRPAzchZ9

并发读写同一个 viper 是否安全?

不安全。你需要自己对 viper 的访问进行同步(例如使用 sync 包)。并发读写会导致 panic。

Troubleshooting

请查看 TROUBLESHOOTING.md

参考资料