Viper——一款Go语言配置项管理工具

323 阅读5分钟

logo.png Viper是适用于Go应用程序(包括Twelve-Factor App)的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。

viper 的特性

  • 支持默认值设置
  • 支持jsonTOMLYAMLHCLenvfileJava properties风格的配置文件类型
  • 支持可选的实时配置文件监控
  • 支持从环境变量读取
  • 支持从远程配置系统中读取
  • 支持命令行读取
  • 支持buffer读取
  • 显式配置值

配置的优先级

  • 显示调用Set设置值
  • 命令行参数(flag)
  • 环境变量
  • 配置文件
  • 远程key/value存储
  • 默认值

注意:配置键的大小写是不敏感的。

配置的读取与加载

在有配置文件的情况下,可以通过SetConfigFile来指定文件名词,然后显式调用ReadInConfig,将配置加载到viper实例中。通过viper实例,我们可以在任意代码段中通过viperGetXXX方法拿到配置。

port: 8123
version: "v1.2.3"
package main

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/spf13/viper"
)

func main() {
	viper.SetConfigFile("config.yaml") // 指定配置文件
	viper.AddConfigPath("./conf/")     // 指定查找配置文件的路径
	err := viper.ReadInConfig()        // 读取配置信息
	if err != nil {                    // 读取配置信息失败
		panic(fmt.Errorf("Fatal error config file: %s \n", err))
	}

	// 监控配置文件变化
	viper.WatchConfig()

	r := gin.Default()
	// 访问/version的返回值会随配置文件的变化而变化
	r.GET("/version", func(c *gin.Context) {
		c.String(http.StatusOK, viper.GetString("version"))
	})

	if err := r.Run(
		fmt.Sprintf(":%d", viper.GetInt("port"))); err != nil {
		panic(err)
	}
}

我更喜欢的使用方式

比起直接操作viper对象,我更喜欢jassue中的封装方式,这里将配置信息封装给配置对象,在使用时直接操作配置对象。当然,这样的方式也有缺点,这里失去了配置的热修改能力。

type Application struct {
    ConfigViper *viper.Viper
    Config config.Configuration
    Log *zap.Logger
    DB *gorm.DB
    Redis *redis.Client
    Cron *cron.Cron
}
func InitializeConfig() *viper.Viper {
    // 设置配置文件路径
    config := "config.yaml"
    if configEnv := os.Getenv("VIPER_CONFIG"); configEnv != "" {
        config = configEnv
    }

    // 初始化 viper
    v := viper.New()
    v.SetConfigFile(config)
    v.SetConfigType("yaml")
    if err := v.ReadInConfig(); err != nil {
        panic(fmt.Errorf("read config failed: %s \n", err))
    }

    // 监听配置文件
    v.WatchConfig()
    v.OnConfigChange(func(in fsnotify.Event) {
        fmt.Println("config file changed:", in.Name)
        // 重载配置
        if err := v.Unmarshal(&global.App.Config); err != nil {
            fmt.Println(err)
        }
    })
    // 将配置赋值给全局变量
    if err := v.Unmarshal(&global.App.Config); err != nil {
       fmt.Println(err)
    }

    return v
}

待解决的疑问

  • 怎样利用viper进行配置项的初始化?
  • viper是以什么样的方式作用在项目中的?
  • 我能从它的源码中学到什么?

Q1:怎样利用viper进行配置项的初始化?

1. 显式设置

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

2. 命令行

我感觉这个法子不好用,所以我直接不学这个。

3. 环境变量

Viper完全支持环境变量。这使Twelve-Factor App开箱即用。有五种方法可以帮助与ENV协作:

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

使用ENV变量时,务必要意识到Viper将ENV变量视为区分大小写。

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

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

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

4. 配置文件

viper.SetConfigFile("./config.yaml") // 指定配置文件路径
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: %s \n", err))
}

5. 远程Key/Value

在Viper中启用远程支持,需要在代码中匿名导入viper/remote这个包。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"

6. 默认值

我认为viper的默认值配置方式和显式配置并没有什么本质区别,只是起标记作用,将显式配置降级使用。

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

Q2:viper是以什么样的方式作用在项目中的?

从面向对象的角度来说,viperViper对象的形式存在于golang程序中,通常以变量指针的形式关联于全局变量中。通过github.com/jassue这个框架项目,我学习到这一点。jassue/global包下,可以直观的看到这一点:

type Application struct {
    ConfigViper *viper.Viper
    Config config.Configuration
    Log *zap.Logger
    DB *gorm.DB
    Redis *redis.Client
    Cron *cron.Cron
}

从功能角度来看,viper是一种内存键值对工具,并具备优先级和热修改能力。

Q3:我能从它的源码中学到什么?

viper实现了一个并不复杂的功能,但是它很实用,这样的案例的学习难度较低,同时对于写一些小项目很有帮助。viper的项目文件采用平铺式目录结构,源码文件注释完备,对于存在分支的位置进行了更详细的描述。

我学到的技巧

  • 单分支语句处,同行注释
  • 多分支语句处,最上方注释,且内部分支不再注释
  • 新增的功能,以新模块的方式加入原始项目,如:remoteinternal

思考与总结

viper适用于小型项目的配置管理,它是否同样适用于构建分布式项目的配置同步。

参考文献