目录
1. viper的介绍
2. viper的使用
2.1 Viper对象的创建
2.2 预设一些默认配置
2.3 从命令行工具的选项参数Flags读取
2.4 从环境变量读取
2.5 从配置文件读取
2.6 从远程key/value存储读取
2.7 监听配置变化
2.8 写入配置到文件
3. 源码分析--配置读取的顺序
4. 参考资料
1. viper的介绍
viper是go一个强大的流行的配置解决方案的库。viper是spf13的另外一个重量级库。有大量项目都使用该库,比如hugo, docker等。 它基本上可以处理所有类型的配置需求和格式, viper支持功能
Viper主要为我们做以下工作:
viepr的安装很简单,直接再工程中使用go get命令安装即可
$ go get github.com/spf13/viper
Viper的是viper库的主要实现对象, viper提供了下面的方法可以获取Viper实例:
func GetViper() *Viper
func New() *Viper
func NewWithOptions(opts ...Option) *Viper
func Sub(key string) *Viper
var v *Viper
func init() {
v = New()
}
// New returns an initialized Viper instance.
func New() *Viper {
v := new(Viper)
v.keyDelim = "."
v.configName = "config"
v.configPermissions = os.FileMode(0o644)
v.fs = afero.NewOsFs()
v.config = make(map[string]interface{})
v.override = make(map[string]interface{})
v.defaults = make(map[string]interface{})
v.kvstore = make(map[string]interface{})
v.pflags = make(map[string]FlagValue)
v.env = make(map[string][]string)
v.aliases = make(map[string]string)
v.typeByDefValue = false
v.logger = jwwLogger{}
v.resetEncoding()
return v
}
func New1() *viper.Viper {
return viper.New()
}
func New2() *viper.Viper {
return viper.NewWithOptions()
}
v := viper.Sub("db")
url := v.Get("url")
log.Printf("mysql url:%s\n", url)
viper.SetDefault("ContentDir", "content")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})
viper.SetDefault("redis.port", 6379)
viper.SetDefault("mysql.url", "root:root@tcp(127.0.0.1:3306)/stock?charset=utf8mb4&parseTime=True&loc=Local")
viper主要提供了以下四个方法,可以绑定行参数的输出的选项值:
func (v *Viper) BindFlagValue(key string, flag FlagValue) error
func (v *Viper) BindFlagValues(flags FlagValueSet) (err error)
func (v *Viper) BindPFlag(key string, flag *pflag.Flag) error
func (v *Viper) BindPFlags(flags *pflag.FlagSet) error
这里我们主要结合之前讲的cobra库中的pflag来讲解一下viper对Flags选项参数的绑定。
在cobra中,我们主要通过cobra.Command来组织不同的命令和子命令,这里我们我通过在root根命令来做测试。代码如下:
func init(){
rootCmd.Flags().String("author", "YOUR NAME", "Author name for copyright attribution")
rootCmd.Flags().String("email", "YOUR EMAIL", "Author email for contact")
// 绑定多个key-value值
viper.BindPFlags(rootCmd.Flags())
// 单个绑定不同的key
viper.BindPFlag("author", rootCmd.Flags().Lookup("author"))
viper.BindPFlag("email", rootCmd.Flags().Lookup("email"))
rootCmd.AddCommand(version.VersionCmd)
}
在cobra的命令的run回调方法中,我们通过viper的来获取输入的选项值
func run(){
fmt.Println("go root cmd run")
fmt.Println(viper.GetString("author"))
fmt.Println(viper.GetString("email"))
}
启动饮用,传入参数测试一下:
go run main.go --author ckeen --email [email protected]
查看一下打印结果,可以看到从viper成功获取到以flag传入的参数值:
➜ cli git:(master) ✗ go run main.go --author keen --email [email protected]
go root cmd run
ckeen
[email protected]
viper支持环境变量的函数:
func (v *Viper) AutomaticEnv() // 开启绑定环境变量
func (v *Viper) BindEnv(input ...string) error // 绑定系统中某个环境变量
func (v *Viper) SetEnvKeyReplacer(r *strings.Replacer)
func (v *Viper) SetEnvPrefix(in string)
使用方法:
func testEnv(){
v := New1()
os.Setenv("CK_HOME","123")
os.Setenv("CK_NAME","ckeen")
v.AutomaticEnv()
//v.BindEnv("SHELL")
v.AllowEmptyEnv(true)
log.Printf("os env:%+v\n", os.Environ())
log.Printf("env: %+v\n", v.Get("HOME"))
log.Printf("env: %+v\n", v.Get("SHELL"))
v.SetEnvPrefix("CK")
log.Printf("ck-home: %+v\n", v.Get("HOME"))
log.Printf("ck-email: %+v\n", v.Get("NAME"))
}
下面我们看一下操作实例, 先看我们的配置文件app.yml文件:
app:
name: viper-test
mode: dev
db:
mysql:
url: "root:root@tcp(127.0.0.1:3306)/stock?charset=utf8mb4&parseTime=True&loc=Local"
redis:
host: 127.0.0.1
port: 6067
db: 0
passwd: 123456
func InitConfig() (*viper.Viper, error) {
v := viper.New()
v.AddConfigPath(".") // 添加配置文件搜索路径,点号为当前目录
v.AddConfigPath("./configs") // 添加多个搜索目录
v.SetConfigType("yaml") // 如果配置文件没有后缀,可以不用配置
v.SetConfigName("app.yml") // 文件名,没有后缀
// v.SetConfigFile("configs/app.yml")
// 读取配置文件
if err := v.ReadInConfig(); err == nil {
log.Printf("use config file -> %s\n", v.ConfigFileUsed())
} else {
return nil,err
}
return v, nil
}
首先这里我们添加一个配置文件搜索路径,点号表示当前路径,搜索路径可以添加多个然后设置了配置文件类型,这里我们设置文件类型为yaml,
接着我们设置了配置文件名称,这个文件可以从配置的搜索路径从查找。
最后我们通过提供的ReadInConfig()函数读取配置文件
// 通过.号来区分不同层级,来获取配置值
log.Printf("app.mode=%s\n", v.Get("app.mode"))
log.Printf("db.mysql.url=%s\n", v.Get("db.mysql.url"))
log.Printf("db.redis.host=%s\n", v.GetString("db.redis.host"))
log.Printf("db.redis.port=%d\n", v.GetInt("db.redis.port"))
// 使用Sub获取子配置,然后获取配置值
v2 := v.Sub("db")
log.Printf("db.mysql.url:%s\n", v2.Sub("mysql").GetString("url"))
log.Printf("db.redis.host:%s\n", v2.Sub("redis").GetString("host"))
log.Printf("db.redis.port:%s\n", v2.Sub("redis").GetInt("port"))
注: 其中重要的一个函数IsSet可以用来判断某个key是否被设置
在Viper中启用远程支持,需要在代码中匿名导入viper/remote
这个包。
_ "github.com/spf13/viper/remote"
Viper将读取从Key/Value存储中的路径检索到的配置字符串(如JSON
、TOML
、YAML
格式)。viper目前支持Consul/Etcd/firestore三种Key/Value的存储系统。下面我来演示从etcd读取配置:
go get github.com/bketelsen/crypt/bin/crypt
crypt set --endpoint=http://127.0.0.1:2379 -plaintext /config/app.yml /Users/ckeen/Documents/code/gosource/go-awesome/go-samples/viper/configs/app.yml
_ "github.com/spf13/viper/remote"
func InitConfigFromRemote() (*viper.Viper,error) {
v := viper.New()
// 远程配置
v.AddRemoteProvider("etcd","http://127.0.0.1:2379","config/app.yml")
//v.SetConfigType("json")
v.SetConfigFile("app.yml")
v.SetConfigType("yml")
if err := v.ReadRemoteConfig(); err == nil {
log.Printf("use config file -> %s\n", v.ConfigFileUsed())
} else {
return nil, err
}
return v, nil
}
func main(){
v, err := InitConfigFromRemote()
if err != nil {
log.Printf("read remote error:%+v\n")
}
log.Printf("remote read app.mode=%+v\n", v.GetString("app.mode"))
log.Printf("remote read db.mysql.url=%+v\n", v.GetString("db.mysql.url"))
}
viper提供如下两种监听配置的函数,一个是本地的监听和一个远程监听的:
func (v *Viper) WatchConfig()
func (v *Viper) WatchRemoteConfig() error
func (v *Viper) WatchRemoteConfigOnChannel() error
我们主要看一下监听本地文件变更的示例
v, err := InitConfig()
if err != nil {
log.Fatalf("viper读取失败, error:%+v\n",err)
}
// 监听到文件变化后的回调
v.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
fmt.Println(v.Get("db.redis.passwd"))
})
v.WatchConfig()
// 阻塞进程退出
time.Sleep(time.Duration(1000000) * time.Second)
我们使用前面的InitConfig()方法来初始化本地文件读取配置,然后设定了监听函数,最后使用WatchConfig()开启本地文件监听。
当我们修改本地配置configs/app.yml的db.redis.passwd的值,然后保存后,我们可以看到控制台有打印最新修改后的值,不要我们重新去获取。
viper提供了如下四个写入配置文件发方法
func (v *Viper) SafeWriteConfig() error
func (v *Viper) SafeWriteConfigAs(filename string) error
func (v *Viper) WriteConfig() error
func (v *Viper) WriteConfigAs(filename string) error
使用SafeWriteConfig()和WriteConfig()时,可以先设定SetConfigFile()设定配置文件的路径。配置写入示例:
v := New1()
v.SetConfigFile("./hello.yml")
log.Printf("config path:%+v\n", v.ConfigFileUsed())
v.SetDefault("author","CKeen")
v.SetDefault("email", "[email protected]")
v.Set("hello", "foo")
v.Set("slice", []string {"slice1","slice2","slice3"})
v.SetDefault("test.web", "https://ckeen.cn")
v.WriteConfig()
//v.WriteConfigAs("./hello.yml")
如果使用SafeWriteConfigAs()或者WriteConfigAs()方法,则直接传入配置文件路径即可。
通过上面的示例我们知道,viper读取配置主要通过一系列Get方法来实现,我们从Get方法跳转到源码可以发现, 主要获取的配置值的为find方法, 方法实现如下:
func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} {
var (
val interface{}
exists bool
path = strings.Split(lcaseKey, v.keyDelim)
nested = len(path) > 1
)
// compute the path through the nested maps to the nested value
if nested && v.isPathShadowedInDeepMap(path, castMapStringToMapInterface(v.aliases)) != "" {
return nil
}
// if the requested key is an alias, then return the proper key
lcaseKey = v.realKey(lcaseKey)
path = strings.Split(lcaseKey, v.keyDelim)
nested = len(path) > 1
// Set() override first
val = v.searchMap(v.override, path)
if val != nil {
return val
}
if nested && v.isPathShadowedInDeepMap(path, v.override) != "" {
return nil
}
// PFlag override next
flag, exists := v.pflags[lcaseKey]
if exists && flag.HasChanged() {
switch flag.ValueType() {
case "int", "int8", "int16", "int32", "int64":
return cast.ToInt(flag.ValueString())
case "bool":
return cast.ToBool(flag.ValueString())
case "stringSlice", "stringArray":
s := strings.TrimPrefix(flag.ValueString(), "[")
s = strings.TrimSuffix(s, "]")
res, _ := readAsCSV(s)
return res
case "intSlice":
s := strings.TrimPrefix(flag.ValueString(), "[")
s = strings.TrimSuffix(s, "]")
res, _ := readAsCSV(s)
return cast.ToIntSlice(res)
case "stringToString":
return stringToStringConv(flag.ValueString())
default:
return flag.ValueString()
}
}
if nested && v.isPathShadowedInFlatMap(path, v.pflags) != "" {
return nil
}
// Env override next
if v.automaticEnvApplied {
// even if it hasn't been registered, if automaticEnv is used,
// check any Get request
if val, ok := v.getEnv(v.mergeWithEnvPrefix(lcaseKey)); ok {
return val
}
if nested && v.isPathShadowedInAutoEnv(path) != "" {
return nil
}
}
envkeys, exists := v.env[lcaseKey]
if exists {
for _, envkey := range envkeys {
if val, ok := v.getEnv(envkey); ok {
return val
}
}
}
if nested && v.isPathShadowedInFlatMap(path, v.env) != "" {
return nil
}
// Config file next
val = v.searchIndexableWithPathPrefixes(v.config, path)
if val != nil {
return val
}
if nested && v.isPathShadowedInDeepMap(path, v.config) != "" {
return nil
}
// K/V store next
val = v.searchMap(v.kvstore, path)
if val != nil {
return val
}
if nested && v.isPathShadowedInDeepMap(path, v.kvstore) != "" {
return nil
}
// Default next
val = v.searchMap(v.defaults, path)
if val != nil {
return val
}
if nested && v.isPathShadowedInDeepMap(path, v.defaults) != "" {
return nil
}
if flagDefault {
// last chance: if no value is found and a flag does exist for the key,
// get the flag's default value even if the flag's value has not been set.
if flag, exists := v.pflags[lcaseKey]; exists {
switch flag.ValueType() {
case "int", "int8", "int16", "int32", "int64":
return cast.ToInt(flag.ValueString())
case "bool":
return cast.ToBool(flag.ValueString())
case "stringSlice", "stringArray":
s := strings.TrimPrefix(flag.ValueString(), "[")
s = strings.TrimSuffix(s, "]")
res, _ := readAsCSV(s)
return res
case "intSlice":
s := strings.TrimPrefix(flag.ValueString(), "[")
s = strings.TrimSuffix(s, "]")
res, _ := readAsCSV(s)
return cast.ToIntSlice(res)
case "stringToString":
return stringToStringConv(flag.ValueString())
default:
return flag.ValueString()
}
}
// last item, no need to check shadowing
}
return nil
}
通过源码,我们可以知道viper读取配置的优先级顺序:alias别名 > 调用Set设置 > flag > env > config > key/value store > default
还有一个注意点:viper配置键不区分大小写,因为viper内部对key统一转为了小写。
viper的包地址:viper package - github.com/spf13/viper - Go Packages
viper的github地址: GitHub - spf13/viper: Go configuration with fangs