Go库笔记:Viper

62 阅读13分钟

【本文章作为Viper代码研究的学习笔记,翻译自官方文档,内容排版有所出入】

Viper - Go配置管理解决方案

Viper是一个Go应用程序配置系统。它旨在在一个应用程序内部工作,提供所有的配置需求。

Viper可以从JSON、TOML、YAML、HCL、envfile和Java属性配置文件等多种格式加载配置,同时支持从环境变量、远程配置系统、命令行标志解析配置。

Viper提供了一些强大的功能:

  • 设置默认配置值
  • 通过配置文件、环境变量等覆盖默认配置
  • 将配置参数绑定到Go变量
  • 热加载配置文件
  • 加密/解密配置值
  • 将ENV变量绑定到配置
  • 从远程配置系统拉取配置
  • 查看使用的配置值
  • 验证配置值

安装

go get github.com/spf13/viper

开始使用

  1. 创建配置文件 config.yaml

  2. 在代码中读取配置:

viper.SetConfigFile("config.yaml")
viper.ReadInConfig()

port := viper.GetString("port")
  1. 访问配置值,例如 viper.GetString("port")

为什么使用Viper

构建一个现代应用程序时,你不希望担心配置文件格式,只想专注于构建出色的软件。Viper正是来帮助您的。

Viper为您做了以下工作:

  1. 找到、加载并解析JSON、TOML、YAML、HCL、INI、envfile或Java属性格式的配置文件。
  2. 为不同的配置选项提供设置默认值的机制。
  3. 提供通过命令行标志覆盖配置的机制。
  4. 提供别名系统,可以轻松重命名参数而不破坏现有代码。
  5. 可以轻松区分用户提供的命令行或配置文件与默认值是否相同。

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

  • Set的显式调用
  • 命令行选项
  • 环境变量
  • 配置文件
  • Key/Value存储
  • 默认值

重要: Viper配置键是大小写不敏感的。

将值放入Viper

建立默认值

一个好的配置系统需要支持默认值。默认值对于没有通过配置文件、环境变量、远程配置或标志设置的键很有用。

例子:

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搜索和读取配置文件。 不需要指定具体路径,但至少需要提供一个期望找到配置文件的路径。

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

写入配置文件

读取配置文件很有用,但有时你会要将运行时的所有修改写入存储。 为此,提供了一系列命令,每个命令都有特定的用途:

  • 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") 

监视和重新读取配置文件

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

不再需要重新启动服务器就可以重载配置,使用viper的应用可以在运行时读取配置更新。

只需要告诉viper实例监视配置即可。 还可以可选地为Viper提供每次变化发生时要运行的函数。

在调用 WatchConfig() 之前,请确保已经添加了所有配置路径

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

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

设置优先覆盖

可以来自命令行标志或应用程序逻辑本身。

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

注册和使用别名

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

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

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

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在读取环境变量时使用前缀。BindEnvAutomaticEnv 都将使用这个前缀。

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

使用ENV变量时,需要认识到的值每次访问时都会读取。Viper不会在调用 BindEnv 时固定值。

AutomaticEnv 是一个强大的辅助程序,特别是与 SetEnvPrefix 组合使用时。调用时,Viper会在每次 viper.Get 请求时检查环境变量。它将应用以下规则:它将检查具有匹配上面描述的规则的环境变量。

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

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

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

Env 示例

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

os.Setenv("SPF_ID", "13") // 通常在应用程序外部完成

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

监视etcd中的更改 - 未加密

// 另外,你可以创建一个新的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) : 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")
} 

访问嵌套键

访问器方法也接受格式化路径以深入访问嵌套键。例如,如果加载以下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() 方法等),则 datastore.metric 的所有子键都变为未定义,它们被更高优先级的配置级别“隐藏”了。

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"

提取子树

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

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

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

生成的代码很容易测试,因为它与主配置结构解耦,更易于重用(出于相同原因)。

解marshal

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

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

  • Unmarshal(rawVal interface{}) : error
  • UnmarshalKey(key string, rawVal interface{}) : 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)
}

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

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还支持解marshal到嵌入式结构中:

/*
示例配置:

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/mitchellh/m… 进行解marshal,默认使用mapstructure标签。

解码自定义格式

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

这在Viper中已经可以使用mapstructure解码钩子实现了。

更多详情请阅读这篇博文

Marshal到字符串

你可能需要将viper持有的所有设置marshal成字符串,而不是写入文件。 你可以对AllSettings()返回的配置使用首选格式的marshaller。

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

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

Viper还是Vipers?

Viper准备好了就可以使用。 不需要配置或初始化就可以开始使用Viper。 由于大多数应用程序希望使用单个集中存储库进行配置,所以viper包提供了这一点。 它类似单例。

在上面的所有示例中,它们都演示了以单例方式使用viper。

使用多个viper

你也可以为应用程序创建许多不同的viper。 每个viper都具有自己唯一的一组配置和值。 每个viper都可以从不同的配置文件、键值存储等读取配置。 Viper包支持的所有功能都镜像为viper的方法。

示例:

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

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

//...

使用多个viper时,跟踪不同的viper是用户的责任。

常见问题解答

为什么称之为“Viper”?

答:Viper旨在成为Cobra伙伴。 虽然两者都可以完全独立运行,但组合在一起它们可以形成一个强大的基础框架。

为什么称之为“Cobra”?

还有比指挥官更好的名字吗?

Viper是否支持大小写敏感键?

简短答案: 不支持。

Viper从各种源合并配置,其中许多源要么不区分大小写,要么与其他源使用不同的大小写(例如环境变量)。 为了在使用多个源时提供最佳体验,已经做出将所有键设为不区分大小写的决定。

我们尝试了几种实现大小写敏感的方法,但遗憾的是这并非易事。 我们可能会在Viper v2中试着实现它,但尽管一开始有些吵闹,它似乎不是很需要。

你可以通过填写此反馈表格来投票支持大小写敏感: forms.gle/R6faU74qPRP…

是否安全地并发读写viper?

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

排障

请参阅 TROUBLESHOOTING.md

开发

为获得最佳的开发体验,建议安装 Nixdirenv

或者,在您的计算机上安装 Go,然后运行 make deps 来安装其余依赖项。

运行测试:

make test

运行linter:

make lint # 传递-j选项以并行运行

某些linter违规可以自动修复:

make fmt

许可证

该项目使用 MIT许可证