Go语言功能齐全的配置管理库-Viper

Viper 是一个功能齐全的 Go 应用程序配置管理库,支持很多场景。它可以处理各种类型的配置需求和格式,包括设置默认值、从多种配置文件和环境变量中读取配置信息、实时监视配置文件等。无论是小型应用还是大型分布式系统,Viper 都可以提供灵活而可靠的配置管理解决方案。

官方仓库地址:https://github.com/spf13/viper 


一、基础知识与快速安装

1、为什么选择Viper?

在构建现代应用程序时,你无需担心配置文件格式;你想要专注于构建出色的软件。Viper的出现就是为了在这方面帮助你的。

Viper能够为你执行下列操作:

  • 查找、加载和反序列化JSON、TOML、YAML、HCL、INI、envfile和Java properties格式的配置文件。

  • 提供一种机制为你的不同配置选项设置默认值。

  • 提供一种机制来通过命令行参数覆盖指定选项的值。

  • 提供别名系统,以便在不破坏现有代码的情况下轻松重命名参数。

  • 当用户提供了与默认值相同的命令行或配置文件时,可以很容易地分辨出它们之间的区别。

Viper 采用以下优先级顺序来加载配置,按照优先级由高到低排序如下:

  • 显式调用 viper.Set 设置的配置值

  • 命令行参数

  • 环境变量

  • 配置文件

  • key/value 存储

  • 默认值

2、Viper的快速安装:

初始化go.mod:

go mod init myviper

安装viper

go get github.com/spf13/viper

如果想指定版本安装:

go get github.com/spf13/viper@v1.10.1

二、读取配置值写入Viper

1、设置默认配置值:

package main

import (
    "fmt"

    "github.com/spf13/viper"
)

func main() {
    // 设置默认配置
    viper.SetDefault("username", "jiguiquan")
    viper.SetDefault("server", map[string]string{"ip": "127.0.0.1", "port": "8080"})

    // 读取配置值
    fmt.Printf("username: %s\n", viper.Get("Username")) // key 不区分大小写
    fmt.Printf("server: %+v\n", viper.Get("server"))
}

运行结果如下:

username: jiguiquan
server: map[ip:127.0.0.1 port:8080]

2、从配置文件读取配置:

Viper 支持从 JSON、TOML、YAML、HCL、INI、envfile 或 Java Properties 格式的配置文件中读取配置。Viper 可以搜索多个路径,但目前单个 Viper 实例只支持单个配置文件。

Viper 不会默认配置任何搜索路径,将默认决定留给应用程序。

主要有两种方式来加载配置文件:

  • 通过 viper.SetConfigFile() 指定配置文件,如果配置文件名中没有扩展名,则需要使用 viper.SetConfigType() 显式指定配置文件的格式。

  • 通过 viper.AddConfigPath() 指定配置文件的搜索路径中,可以通过多次调用,来设置多个配置文件搜索路径。然后通过 viper.SetConfigName() 指定不带扩展名的配置文件,Viper 会根据所添加的路径顺序查找配置文件,如果找到就停止查找。

先准备一个 config.yaml 配置文件:

username: jiguiquan
password: 123456
server:
  ip: 127.0.0.1
  port: 8080

在准备一个 config.properties 配置文件:

username=zidan
password=123456
server.ip=127.0.0.1
server.port=8080

编写如下 go 程序:

package main

import (
    "errors"
    "flag"
    "fmt"

    "github.com/spf13/viper"
)

var (
    // 启动时,通过 -c 标识,传入配置文件路径
    cfg = flag.String("c", "", "config file.")
)

func main() {
    flag.Parse()

    if *cfg != "" {
        viper.SetConfigFile(*cfg)   // 指定配置文件(路径 + 配置文件名)
        //viper.SetConfigType("yaml") // 如果配置文件名中没有扩展名,则需要显式指定配置文件的格式
    } else {
        viper.AddConfigPath(".")             // 把当前目录加入到配置文件的搜索路径中
        viper.AddConfigPath("$HOME/.config") // 可以多次调用 AddConfigPath 来设置多个配置文件搜索路径
        viper.SetConfigName("config")           // 指定配置文件名(没有扩展名,会自己找,找到就停止)
    }

    // 读取配置文件
    if err := viper.ReadInConfig(); err != nil {
        if _, ok := err.(viper.ConfigFileNotFoundError); ok {
            fmt.Println(errors.New("config file not found"))
        } else {
            fmt.Println(errors.New("config file was found but another error was produced"))
        }
        return
    }

    fmt.Printf("using config file: %s\n", viper.ConfigFileUsed())

    // 读取配置值
    fmt.Printf("username: %s\n", viper.Get("username"))
}

验证1:指定配置文件读取:

E:\StudyGo\myviper>go run main.go -c ./config.properties
using config file: ./config.properties
username: zidan

验证2:不指定配置文件读取:

E:\StudyGo\myviper>go run main.go
using config file: E:\StudyGo\myviper\config.yaml
username: jiguiquan

3、监控并重新读取配置文件:

Viper 支持在应用程序运行过程中实时读取配置文件,即热加载配置。

只需要调用 viper.WatchConfig() 即可开启此功能。

编写 go 程序:

package main

import (
    "fmt"
    "time"

    "github.com/fsnotify/fsnotify"
    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigFile("./config.yaml")
    viper.ReadInConfig()

    // 注册每次配置文件发生变更后都会调用的回调函数
    // 很多编辑器都会触发2次修改事件,注意回调逻辑的幂等性
    viper.OnConfigChange(func(e fsnotify.Event) {
        fmt.Printf("配置文件发生改变: %s\n", e.Name)
    })

    // 重要:监控并重新读取配置文件,需要确保在调用前添加了所有的配置路径
    viper.WatchConfig()

    // 每隔10秒打印一次username配置最新值
    for {
        // 读取配置值
        fmt.Printf("username: %s\n", viper.Get("username"))
        time.Sleep(time.Second * 10)
    }
}

运行结果:

E:\StudyGo\myviper>go run main.go
username: jiguiquan
配置文件发生改变: config.yaml
username: zidan
username: zidan
username: zidan

经过测试:当使用 Goland/Vscode/vim/vi 等编辑器编辑文件时,都会触发两次修改事件,当使用记事本修改时,只会触发一次事件;

官方解释是:编辑器的原因,编辑器发出了两次修改事件!

所以我们在开发时,要考虑到回调函数中的处理逻辑跌幂等性!

4、从 io.Reader 读取配置:

Viper 支持从任何实现了 io.Reader 接口的配置源中读取配置!

package main

import (
    "bytes"
    "fmt"

    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigType("yaml") // 或者使用 viper.SetConfigType("YAML")

    var yamlExample = []byte(`
        username: jiguiquan
        password: 123456
        server:
          ip: 127.0.0.1
          port: 8080
    `)

    // bytes.NewBuffer() 构造了一个 bytes.Buffer 对象
    // 而 bytes.NewBuffer() 是实现了 io.Reader 接口的
    viper.ReadConfig(bytes.NewBuffer(yamlExample))

    // 读取配置值
    fmt.Printf("username: %s\n", viper.Get("username"))
}

运行结果如下:

E:\StudyGo\myviper>go run main.go
username: jiguiquan

5、从环境变量读取配置:

注意 ⚠:Viper 在读取环境变量时,是区分大小写的。

Viper 还支持从环境变量读取配置,有 5 个方法可以帮助我们使用环境变量:

  • AutomaticEnv():可以绑定全部环境变量(用法上类似 flag 包的 flag.Parse())。调用后,Viper 会自动检测和加载所有环境变量。

  • BindEnv(string…) : error:绑定一个环境变量。需要一个或两个参数,第一个参数是配置项的键名,第二个参数是环境变量的名称。如果未提供第二个参数,则 Viper 将假定环境变量名为:环境变量前缀_键名,且为全大写形式。例如环境变量前缀为 ENV,键名为 username,则环境变量名为 ENV_USERNAME。当显式提供第二个参数时,它不会自动添加前缀,也不会自动将其转换为大写。例如,使用 viper.BindEnv("username", "username") 绑定键名为 username 的环境变量,应该使用 viper.Get("username") 读取环境变量的值。

    在使用环境变量时,需要注意,每次访问它的值时都会去环境变量中读取。当调用 BindEnv 时,Viper 不会缓存它的值

  • SetEnvPrefix(string):可以告诉 Viper 在读取环境变量时使用的前缀。BindEnv 和 AutomaticEnv 都将使用此前缀。例如,使用 viper.SetEnvPrefix("ENV") 设置了前缀为 ENV,并且使用 viper.BindEnv("username") 绑定了环境变量,在使用 viper.Get("username") 读取环境变量时,实际读取的 key 是 ENV_USERNAME。

  • SetEnvKeyReplacer(string…) *strings.Replacer:允许使用 strings.Replacer 对象在一定程度上重写环境变量的键名。例如,存在 SERVER_IP="127.0.0.1" 环境变量,使用 viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) 将键名中的 . 或 – 替换成 _,则通过 viper.Get("server_ip")、viper.Get("server.ip")、viper.Get("server-ip") 三种方式都可以读取环境变量对应的值。

  • AllowEmptyEnv(bool):当环境变量为空时(有键名而没有值的情况),默认会被认为是未设置的,并且程序将回退到下一个配置来源。要将空环境变量视为已设置,可以使用此方法。

package main

import (
    "fmt"
    "strings"

    "github.com/spf13/viper"
)

/** 需要先配置以下环境变量值
export username=jgq
export ENV_USERNAME=jiguiquan
export password=jgq123
export ENV_PASSWORD=jiguiquan123
export ENV_SERVER_IP=127.0.0.1
 */
func main() {
    viper.SetEnvPrefix("env") // 设置读取环境变量前缀,会自动转为大写 ENV
    viper.AllowEmptyEnv(true) // 将空环境变量视为已设置

    //viper.AutomaticEnv()      // 可以绑定全部环境变量
    viper.BindEnv("username")             // 自动绑定到ENV_USERNAME环境变量
    viper.BindEnv("password","password")  // 绑定到password环境变量
    viper.BindEnv("server.ip")            // 自动绑定到ENV_SERVER_IP环境变量

    // 将键名中的 . 或 - 替换成 _
    viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))

    // 读取配置
    fmt.Printf("username: %v\n", viper.Get("username"))
    fmt.Printf("password: %v\n", viper.Get("password"))
    fmt.Printf("server.ip: %v\n", viper.Get("server.ip"))

    // 读取全部配置,只能获取到通过 BindEnv 绑定的环境变量,无法获取到通过 AutomaticEnv 绑定的环境变量
    fmt.Println(viper.AllSettings())
}

注意,在运行程序前,先设置代码前的那几个环境变量:

运行结果如下:

[root@tcosmo-szls01 myviper]# go run main.go 
username: jiguiquan        # 读取的是ENV_USERNAME的值
password: jgq123           # 读取的是password的值
server.ip: 127.0.0.1       # 读取的是ENV_SERVER_IP的值
map[password:jgq123 server:map[ip:127.0.0.1] username:jiguiquan]  # 只打印了通过 BindEnv 绑定的环境变量

6、从命令行参数读取配置:

Viper 支持 pflag 包(它们其实都在 spf13 仓库下),能够绑定命令行标志,从而读取命令行参数。

同 BindEnv 类似,在调用绑定方法时,不会设置值,而是在每次访问时设置。这意味着我们可以随时绑定它,例如可以在 init() 函数中。

  • BindPFlag:对于单个标志,可以调用此方法进行绑定。

  • BindPFlags:可以绑定一组现有的标志集 pflag.FlagSet。

package main

import (
    "fmt"

    "github.com/spf13/pflag"
    "github.com/spf13/viper"
)

var (
    // 自带的Flag是不支持 shorthand 标识简写的
    username = pflag.StringP("username", "u", "", "help message for username")
    password = pflag.StringP("password", "p", "", "help message for password")
)

func main() {
    // pflag 用法几乎和 Flag一样,兼容Flag
    pflag.Parse()

    viper.BindPFlag("username", pflag.Lookup("username")) // 绑定单个标志
    viper.BindPFlags(pflag.CommandLine)                   // 绑定标志集

    // 读取配置值
    fmt.Printf("username: %s\n", viper.Get("username"))
    fmt.Printf("password: %s\n", viper.Get("password"))
}

运行结果如下:

E:\StudyGo\myviper>go run main.go --username=jiguiquan -p 123456
username: jiguiquan
password: 123456

7、从远程 key/value 存储读取配置:

通过阅读 viper.go 的源码,可以知道,暂时支持的远程provider有三种:etcd、consul、firestore

// provider is a string value: "etcd", "consul" or "firestore" are currently supported.
func AddRemoteProvider(provider, endpoint, path string) error {
   return v.AddRemoteProvider(provider, endpoint, path)
}

我们快速地在本地运行一个Consul:https://developer.hashicorp.com/consul/downloads

解压后直接以开发模式运行:

consul.exe agent -dev       # 开发模式运行
consul.exe agent -server    # 服务器模式运行

启动后,直接访问 http://localhost:8500 即可访问 Consul 界面:

1695104202967373.png

我们在 user/config 创建一组yaml格式的配置:

1695104364424276.png

配置准备好后,我们就可以开始编写 go 程序了:

package main

import (
    "fmt"

    "github.com/spf13/viper"
    _ "github.com/spf13/viper/remote" // 必须导入,才能加载远程 key/value 配置
)

func main() {
    viper.AddRemoteProvider("consul", "localhost:8500", "user/config") // 连接远程 consul 服务
    viper.SetConfigType("YAML")     // 显式设置文件格式文 YAML
    viper.ReadRemoteConfig()

    // 读取配置值
    fmt.Printf("username: %s\n", viper.Get("username"))
    fmt.Printf("server.ip-port: %s-%d\n", viper.Get("server.ip"), viper.Get("server.port"))
}

在运行时,可能需要安装对应的remote:

go get github.com/spf13/viper/remote
# 或
go get github.com/spf13/viper/remote@v1.10.1

运行结果如下:

E:\StudyGo\myviper>go run main.go
username: zidan-from-consul
server.ip-port: 127.0.0.1-5600

正常能要用到读取场景也差不多就是上面这些!


三、从 Viper 中读取配置值

在 Viper 中,有如下几种方法可以获取配置值:

  • Get(key string) interface{}:获取配置项 key 所对应的值,key 不区分大小写,返回接口类型。

  • Get<Type>(key string) <Type>:获取指定类型的配置值, 可以是 Viper 支持的类型:GetBool、GetFloat64、GetInt、GetIntSlice、GetString、GetStringMap、GetStringMapString、GetStringSlice、GetTime、GetDuration。

  • AllSettings() map[string]interface{}:返回所有配置。根据我的经验,如果使用环境变量指定配置,则只能获取到通过 BindEnv 绑定的环境变量,无法获取到通过 AutomaticEnv 绑定的环境变量。

  • IsSet(key string) bool:值得注意的是,在使用 Get 或 Get<Type> 获取配置值,如果找不到,则每个 Get 函数都会返回一个零值。为了检查给定的键是否存在,可以使用 IsSet 方法,存在返回 true,不存在返回 false。

1、访问嵌套型key的配置项:

username: jiguiquan
password: 123456
server:
  ip: 127.0.0.1
  port: 8080
server.ip: 10.0.0.1  # 直接的 server.ip 优先级会高于嵌套型的配置值

编写的测试 go 代码如下:

package main

import (
    "fmt"

    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigFile("./config.yaml")
    viper.ReadInConfig()

    // 读取配置值
    fmt.Printf("username: %v\n", viper.Get("username"))
    fmt.Printf("server: %v\n", viper.Get("server"))
    fmt.Printf("server.ip: %v\n", viper.Get("server.ip"))
    fmt.Printf("server.port: %v\n", viper.Get("server.port"))
}

运行结果如下:

E:\StudyGo\myviper>go run main.go
username: jiguiquan
server: map[ip:127.0.0.1 port:8080]
server.ip: 10.0.0.1
server.port: 8080

2、获取配置树,逐层获取配置值(树中的配置项不会被1中的场景覆盖):

在刚刚的代码中加上几行:

package main

import (
    "fmt"

    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigFile("./config.yaml")
    viper.ReadInConfig()

    // 读取配置值
    fmt.Printf("username: %v\n", viper.Get("username"))
    fmt.Printf("server: %v\n", viper.Get("server"))
    fmt.Printf("server.ip: %v\n", viper.Get("server.ip"))
    fmt.Printf("server.port: %v\n", viper.Get("server.port"))

    // 获取 server 子树
    srvCfg := viper.Sub("server")
    fmt.Printf("ip: %v\n", srvCfg.Get("ip"))
    fmt.Printf("port: %v\n", srvCfg.Get("port"))
}

运行结果如下(很容易理解):

username: jiguiquan
server: map[ip:127.0.0.1 port:8080]
server.ip: 10.0.0.1
server.port: 8080
ip: 127.0.0.1
port: 8080

3、配置文件的反序列化:

Viper 提供了 2 个方法进行反序列化操作,以此来实现将所有或特定的值解析到结构体、map 等。

  • Unmarshal(rawVal interface{}) : error:反序列化所有配置项。

  • UnmarshalKey(key string, rawVal interface{}) : error:反序列化指定配置项。

package main

import (
    "fmt"

    "github.com/spf13/viper"
)

type Config struct {
    Username string
    Password string
    // Viper 支持嵌套结构体
    Server struct {
        IP   string
        Port int
    }
}

func main() {
    viper.SetConfigFile("./config.yaml")
    viper.ReadInConfig()

    // 将配置文件反序列化为 cfg 对象
    var cfg *Config
    if err := viper.Unmarshal(&cfg); err != nil {
        panic(err)
    }

    // 将单个key字段,反序列化为password字符串
    var password *string
    if err := viper.UnmarshalKey("password", &password); err != nil {
        panic(err)
    }

    fmt.Printf("cfg: %+v\n", cfg)
    fmt.Printf("password: %s\n", *password)
}

运行结果如下:

cfg: &{Username:jiguiquan Password:123456 Server:{IP:127.0.0.1 Port:8080}}
password: 123456

4、序列化:

一个好用的配置包不仅能够支持反序列化操作,还要支持序列化操作。Viper 支持将配置序列化成字符串,或直接序列化到文件中;

package main

import (
    "fmt"

    "github.com/spf13/viper"
)


func main() {
    viper.Set("username", "jiguiquan")
    viper.Set("password", "123")
    viper.Set("server", map[string]string{"ip": "192.168.0.1", "port": "8080"})

    fmt.Println("Viper的所有配置如下:", viper.AllSettings())
    viper.SafeWriteConfigAs("./myconfig.yaml")
}

执行效果如下:

Viper的所有配置如下: map[password:123 server:map[ip:192.168.0.1 port:8080] username:jiguiquan]

同时还会生成一个配置文件:myconfig.yaml,内容如下:

password: "123"
server:
  ip: 127.0.0.1
  port: "8080"
username: jiguiquan

四、多实例对象

由于大多数应用程序都希望使用单个配置实例对象来管理配置,因此 viper 包默认提供了这一功能,它类似于一个单例。当我们使用 Viper 时不需要配置或初始化,Viper 实现了开箱即用的效果。

在上面的所有示例中,演示了如何以单例方式使用 Viper。我们还可以创建多个不同的 Viper 实例以供应用程序中使用,每个实例都有自己单独的一组配置和值,并且它们可以从不同的配置文件、key/value 存储等位置读取配置信息。

Viper 包支持的所有功能都被镜像为 viper 对象上的方法,这种设计思路在 Go 语言中非常常见,如标准库中的 log 包。

多实例使用示例:

package main

import (
    "fmt"

    "github.com/spf13/viper"
)

func main() {
    x := viper.New()
    y := viper.New()

    x.SetConfigFile("./config.yaml")
    x.ReadInConfig()
    fmt.Printf("x.username: %v\n", x.Get("username"))

    y.SetDefault("username", "吉桂权")
    fmt.Printf("y.username: %v\n", y.Get("username"))
}

执行效果如下:

x.username: jiguiquan
y.username: 吉桂权

五、项目实战建议

  • 对于小型项目:推荐直接使用 viper 实例管理配置

  • 对于大型项目:定义一个用来记录配置的结构体,使用 Viper 将配置反序列化到结构体中

但是当我们使用 viper.WatchConfig() 监听配置文件变化,如果配置变化,则变化会立刻体现在 viper 实例对象上,但是并不会改变我们的“结构体配置对象”,需要需要在 viper.OnConfigChange 的回调函数中,将变更好的最新值更新到“结构体配置对象”中。

jiguiquan@163.com

文章作者信息...

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐