Golang Viper包 配置管理

1,438 阅读6分钟

viper

Viper是一个应用程序配置系统

支持的配置方式优先级(由高到低)如下

  • flag:读取命令行指定的参数,设置配置值
  • env:通过环境变量,设置配置值
  • config:读取本地配置文件,设置配置值
  • key/value store:读取远程配置文件,设置配置值
  • default:设置默认值

为什么使用Viper

  • 支持多种配置方式
  • 对于配置文件方式,支持多种配置文件格式: "json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "dotenv", "env", "ini"
  • 对于远程服务,同样支持多种方式: "etcd", "consul", "firestore"
  • 支持反序列化到结构体,绑定解析出来的配置值

安装

安装viper

$ mkdir example && cd example

$ go env -w GO111MODULE=on

$ go env -w GOPROXY=https://goproxy.cn,direct

$  go mod init viper-test
go: creating new go.mod: module viper-test
go: to add module requirements and sums:
	go mod tidy
$ go get github.com/spf13/viper
go get: added github.com/spf13/viper v1.8.1

安装etcd

wget https://github.com/etcd-io/etcd/releases/download/v2.3.8/etcd-v2.3.8-linux-amd64.tar.gz
tar zxvf etcd-v2.3.8-linux-amd64.tar.gz
mv etcd-v2.3.8-linux-amd64 etcd-v2.3.8
cd etcd-v2.3.8

运行./etcd启动,在另起一个终端,进入到etcd-v2.3.8目录,执行如下命令,写入配置内容

ETCDCTL_API=2 ./etcdctl set /configs/config.json <<EOF
{
  "source": "key/value store",
  "account": {
    "name": "antfoot1",
    "age": 28
  }
}
EOF

设置默认值

package main

import (
	"fmt"
	"github.com/spf13/viper"
	_ "github.com/spf13/viper/remote"
)
var newViper *viper.Viper

func main() {
	newViper = viper.New()
	fmt.Println("=====设置默认值方式=====")
	SetDefault()

	source := newViper.Get("source")
	mysql := newViper.Get("mysql")
	user := newViper.Get("user")
	account := newViper.Get("account")

	fmt.Printf("source: %T, %v, \n", source, source)
	fmt.Printf("mysql: %T, %v, \n", mysql, mysql)
	fmt.Printf("account的别名user: %T, %v, \n", user, user)
	fmt.Printf("account: %T, %v, \n", account, account)
}

func SetDefault() {
	newViper.SetDefault("SOURCE", "default")
	newViper.SetDefault("mysql.host", "127.0.0.1")
	newViper.RegisterAlias("user", "account")
	newViper.SetDefault("user", map[string]interface{} {
		"Name": "AntFoot",
		"age": 18,
	})
}

运行结果如下

# go run main.go
=====设置默认值方式=====
source: string, default,
mysql: map[string]interface {}, map[host:127.0.0.1],
account的别名user: map[string]interface {}, map[age:18 name:AntFoot],
account: map[string]interface {}, map[age:18 name:AntFoot],
  • 配置键不区分大小写,newViper.SetDefault("SOURCE", "default")设置SOURCE值为default,获取配置source := newViper.Get("source")使用小写source,这是由于在设置key之前,通过strings.ToLower(key)进行了格式转换
  • Viper默认分隔符v.keyDelim = ".",根据分隔符解析key的内容为map结构,newViper.SetDefault("mysql.host", "127.0.0.1")解析结果为map[string]interface {}, map[host:127.0.0.1]
  • 自定义分隔符,则可以通过如下方式定义newViper = viper.NewWithOptions(viper.KeyDelimiter("-"))替换newViper = viper.New()
  • newViper.RegisterAlias("user", "account") 注册别名

设置默认值 + 读取远程文件设置配置方式(etcd)

当前etcd中的配置内容

# ./etcdctl get /configs/config.json
{
  "source": "key/value store",
  "account": {
    "name": "antfoot1",
    "age": 28
  }
}

编辑main.go文件,添加设置读取远程文件的配置方式

package main

import (
	"fmt"
	"github.com/spf13/viper"
	_ "github.com/spf13/viper/remote"
)
var newViper *viper.Viper

func main() {
	newViper = viper.New()
	fmt.Println("=====key/value store=====")
	SetKeyValStore()
	
	fmt.Println("=====设置默认值方式=====")
	SetDefault()

	source := newViper.Get("source")
	mysql := newViper.Get("mysql")
	user := newViper.Get("user")
	account := newViper.Get("account")

	fmt.Printf("source: %T, %v, \n", source, source)
	fmt.Printf("mysql: %T, %v, \n", mysql, mysql)
	fmt.Printf("account的别名user: %T, %v, \n", user, user)
	fmt.Printf("account: %T, %v, \n", account, account)
}

func SetKeyValStore() {
	if err := newViper.AddRemoteProvider("etcd", "http://127.0.0.1:2379","/configs/config.json"); err != nil {
		fmt.Println("add remote provider: ",  err)
		return
	}
	newViper.SetConfigType("json")
	if err := newViper.ReadRemoteConfig();err != nil {
		fmt.Println("read remote config: ",  err)
		return
	}
}

func SetDefault() {
	newViper.SetDefault("SOURCE", "default")
	newViper.SetDefault("mysql.host", "127.0.0.1")
	newViper.RegisterAlias("user", "account")  // 注册别名
	newViper.SetDefault("user", map[string]interface{} {
		"Name": "AntFoot",
		"age": 18,
	})
}

运行结果

# go run main.go
=====key/value store=====
=====设置默认值方式=====
source: string, key/value store,
mysql: map[string]interface {}, map[host:127.0.0.1],
account的别名user: map[string]interface {}, map[age:28 name:antfoot1],
account: map[string]interface {}, map[age:28 name:antfoot1],

先执行SetKeyValStore函数,读取etcd中的配置,并设置,然后执行SetDefault函数,设置默认值

打印结果source: string, key/value store, source字段的值为key/value store,name和account字段的值也被更新为etcd中的值

读取远程配置文件,设置配置值方式优先级高于设置默认值

同时使用所有配置方式

main.go同级目录下创建config.json文件,编写如下配置

{
  "source": "config",
  "config": "config.json"
}

编辑main.go文件如下

package main

import (
	"flag"
	"fmt"
	"github.com/spf13/pflag"
	"github.com/spf13/viper"
	_ "github.com/spf13/viper/remote"
	"os"
)
var newViper *viper.Viper

func main() {
	newViper = viper.New()

	fmt.Println("=====flag=====")
	SetFlag()

	fmt.Println("=====env环境变量=====")
	SetEnv()

	fmt.Println("=====config配置文件方式=====")
	SetConfig()

	fmt.Println("=====key/value store=====")
	SetKeyValStore()

	fmt.Println("=====设置默认值方式=====")
	SetDefault()

	source := newViper.Get("source")
	mysql := newViper.Get("mysql")
	user := newViper.Get("user")
	account := newViper.Get("account")
	config := newViper.Get("config")
	age := newViper.Get("age")

	fmt.Printf("source: %T, %v, \n", source, source)
	fmt.Printf("mysql: %T, %v, \n", mysql, mysql)
	fmt.Printf("account的别名user: %T, %v, \n", user, user)
	fmt.Printf("account: %T, %v, \n", account, account)
	fmt.Printf("来自config方式 - config: %T, %v, \n", config, config)
	fmt.Printf("来自flag方式 - age: %T, %v, \n", age, age)
}

// 使用标准库 "flag" 包
func SetFlag() {
	flag.Int("age", 18, "年龄")

	pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
	pflag.Parse()
	if err := newViper.BindPFlags(pflag.CommandLine); err != nil {
		fmt.Println("fail to bind pflags err", err)
		return
	}
}

// 使用环境变量
func SetEnv() {
	newViper.SetEnvPrefix("spf")
	if err := newViper.BindEnv("source"); err != nil {
		fmt.Println("bind env err", err)
	}

	if err := os.Setenv("SPF_SOURCE", "env"); err != nil {
		fmt.Println("set env err ", err)
	}
}

func SetConfig() {
	newViper.SetConfigFile("config.json")
	// 查找并读取配置文件
	if err := newViper.ReadInConfig(); err != nil {
		fmt.Println("fail to set config ", err)
		return
	}
}

func SetKeyValStore() {
	if err := newViper.AddRemoteProvider("etcd", "http://127.0.0.1:2379","/configs/config.json"); err != nil {
		fmt.Println("add remote provider: ",  err)
		return
	}
	newViper.SetConfigType("json")
	if err := newViper.ReadRemoteConfig();err != nil {
		fmt.Println("read remote config: ",  err)
		return
	}
}

func SetDefault() {
	newViper.SetDefault("SOURCE", "default")
	newViper.SetDefault("mysql.host", "127.0.0.1")
	newViper.RegisterAlias("user", "account")  // 注册别名
	newViper.SetDefault("user", map[string]interface{} {
		"Name": "AntFoot",
		"age": 18,
	})
}

运行结果

# go run main.go --age=18
=====flag=====
=====env环境变量=====
=====config配置文件方式=====
=====key/value store=====
=====设置默认值方式=====
source: string, env,
mysql: map[string]interface {}, map[host:127.0.0.1],
account的别名user: map[string]interface {}, map[age:28 name:antfoot1],
account: map[string]interface {}, map[age:28 name:antfoot1],
来自config方式 - config: string, config.json,
来自flag方式 - age: int, 18,

Viper结构体

Viper定义的结构体如下

type Viper struct {
	// 分隔符,分隔用于一次性访问嵌套值的键列表
	keyDelim string
	
	// 查找配置文件的一组路径
	configPaths []string

	// 读取配置的文件系统
	fs afero.Fs

	// 提供一组用于搜索配置的远程程序
	remoteProviders []*defaultRemoteProvider

	// 要在路径中查找的文件的名称
	configName        string
	configFile        string
	configType        string
	configPermissions os.FileMode
	envPrefix         string

	// 用于ini解析的特定命令
	iniLoadOptions ini.LoadOptions

	automaticEnvApplied bool
	envKeyReplacer      StringReplacer
	allowEmptyEnv       bool

	config         map[string]interface{}
	override       map[string]interface{}
	defaults       map[string]interface{}	// 存储默认设置
	kvstore        map[string]interface{}	// 保存远程服务配置
	pflags         map[string]FlagValue
	env            map[string][]string
	aliases        map[string]string
	typeByDefValue bool

	// 在对象上存储读取属性,以便我们可以按注释顺序写回。
	// 仅当配置读取为属性文件时,才会使用此选项。
	properties *properties.Properties

	onConfigChange func(fsnotify.Event)
}

Viper支持的多种配置,每个配置的读取之后,保存到如下属性字段中

  • config 保存配置文件方式读取到的配置内容
  • defaults 保存设置的默认值
  • kvstore 保存远程服务读取的配置
  • pflags 保存通过flag方式获取的配置
  • env 保存通过环境变量设置的配置

在读取配置的时候 newViper.Get("source"),会按照优先级,以此读取对应的属性字段的内容,如果存在则返回,否则继续向下查找

读取配置,反序列化到结构体

修改main.go文件,添加反序列化,完整代码如下

package main

import (
	"flag"
	"fmt"
	"github.com/spf13/pflag"
	"github.com/spf13/viper"
	_ "github.com/spf13/viper/remote"
	"os"
)

type Mysql struct {
	Host string `mapstructure:"host"`
}

type Account struct {
	Name string `mapstructure:"name"`
	Age int `mapstructure:"age"`
}

type GlobalConfig struct {
	Source string `mapstructure:"source"`
	Mysql Mysql `mapstructure:"mysql"`
	Account Account `mapstructure:"account"`
	User Account `mapstructure:"user"`
	Config string `mapstructure:"config"`
	Age int  `mapstructure:"age"`
}

var newViper *viper.Viper
var c GlobalConfig

func main() {
	newViper = viper.New()

	fmt.Println("=====flag=====")
	SetFlag()

	fmt.Println("=====env环境变量=====")
	SetEnv()

	fmt.Println("=====config配置文件方式=====")
	SetConfig()

	fmt.Println("=====key/value store=====")
	SetKeyValStore()

	fmt.Println("=====设置默认值方式=====")
	SetDefault()

	source := newViper.Get("source")
	mysql := newViper.Get("mysql")
	user := newViper.Get("user")
	account := newViper.Get("account")
	config := newViper.Get("config")
	age := newViper.Get("age")

	fmt.Printf("source: %T, %v, \n", source, source)
	fmt.Printf("mysql: %T, %v, \n", mysql, mysql)
	fmt.Printf("account的别名user: %T, %v, \n", user, user)
	fmt.Printf("account: %T, %v, \n", account, account)
	fmt.Printf("来自config方式 - config: %T, %v, \n", config, config)
	fmt.Printf("来自flag方式 - age: %T, %v, \n", age, age)
	if err := newViper.Unmarshal(&c); err != nil {
		fmt.Println("反序列化 err ", err)
		return
	}
	fmt.Printf("反序列化结果: %T, %+v \n", c, c)
}

// 使用标准库 "flag" 包
func SetFlag() {
	flag.Int("age", 18, "年龄")

	pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
	pflag.Parse()
	if err := newViper.BindPFlags(pflag.CommandLine); err != nil {
		fmt.Println("fail to bind pflags err", err)
		return
	}
}

// 使用环境变量
func SetEnv() {
	newViper.SetEnvPrefix("spf")
	if err := newViper.BindEnv("source"); err != nil {
		fmt.Println("bind env err", err)
	}

	if err := os.Setenv("SPF_SOURCE", "env"); err != nil {
		fmt.Println("set env err ", err)
	}
}

func SetConfig() {
	newViper.SetConfigFile("config.json")
	// 查找并读取配置文件
	if err := newViper.ReadInConfig(); err != nil {
		fmt.Println("fail to set config ", err)
		return
	}
}

func SetKeyValStore() {
	if err := newViper.AddRemoteProvider("etcd", "http://127.0.0.1:2379","/configs/config.json"); err != nil {
		fmt.Println("add remote provider: ",  err)
		return
	}
	newViper.SetConfigType("json")
	if err := newViper.ReadRemoteConfig();err != nil {
		fmt.Println("read remote config: ",  err)
		return
	}
}

func SetDefault() {
	newViper.SetDefault("SOURCE", "default")
	newViper.SetDefault("mysql.host", "127.0.0.1")
	newViper.RegisterAlias("user", "account")  // 注册别名
	newViper.SetDefault("user", map[string]interface{} {
		"Name": "AntFoot",
		"age": 18,
	})
}

运行结果

root@e9e43e92bedb:/data/packages/viper/example# go run main.go --age=18
=====flag=====
=====env环境变量=====
=====config配置文件方式=====
=====key/value store=====
=====设置默认值方式=====
source: string, env,
mysql: map[string]interface {}, map[host:127.0.0.1],
account的别名user: map[string]interface {}, map[age:28 name:antfoot1],
account: map[string]interface {}, map[age:28 name:antfoot1],
来自config方式 - config: string, config.json,
来自flag方式 - age: int, 18,
反序列化结果: main.GlobalConfig, {Source:env Mysql:{Host:127.0.0.1} Account:{Name:antfoot1 Age:28} User:{Name:antfoot1 Age:28} Config:config.json Age:18}

可以看到,通过newViper.Unmarshal(&c)解析到结构体中的数据和通过newViper.Get方法查询的结果是一样的