Go项目配置管理神器之viper的介绍与使用详解
来源:脚本之家
时间:2023-02-25 08:56:11 303浏览 收藏
小伙伴们对Golang编程感兴趣吗?是否正在学习相关知识点?如果是,那么本文《Go项目配置管理神器之viper的介绍与使用详解》,就很适合你,本篇文章讲解的知识点主要包括Viper、配置管理。在之后的文章中也会多多分享相关知识点,希望对大家的知识积累有所帮助!
1. viper的介绍
viper是go一个强大的流行的配置解决方案的库。viper是spf13的另外一个重量级库。有大量项目都使用该库,比如hugo, docker等。 它基本上可以处理所有类型的配置需求和格式, viper支持功能
- 设置默认配置
- 支持各种配置文件,如JSON,TOML, YAML, HCL, envfile和Java属性配置文件
- 支持监听文件变化以及重新读取配置
- 支持从环境变量读取配置
- 支持从远程配置系统(etcd或Consul)读取配置,并能监听远程配置修改
- 支持从命令行标志Flag读取配置,比如搭配cobra使用
- 支持读取缓冲区数据
Viper主要为我们做以下工作:
- 查找、加载和解组JSON、TOML、YAML、HCL、INI、envfile或Java属性格式的配置文件。
- 提供一种机制来为不同的配置选项设置默认值。
- 提供一种机制来为通过命令行标志指定的选项设置覆盖值。
- 提供别名系统,以便在不破坏现有代码的情况下轻松重命名参数。
- 当用户提供了与默认值相同的命令行或配置文件时,很容易区分它们。
viepr的安装很简单,直接再工程中使用go get命令安装即可
$ go get github.com/spf13/viper
2. viper的使用
2.1 Viper对象的创建
Viper的是viper库的主要实现对象, viper提供了下面的方法可以获取Viper实例:
func GetViper() *Viper func New() *Viper func NewWithOptions(opts ...Option) *Viper func Sub(key string) *Viper
使用viper.GetViper()获取的为全局的Viper实例对象,默认使用viper包使用也是该全局Viper实例。查看viper的源码,可以看到viper默认提供了一个全局的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 }
New和NewWithOptions为我们提供了创建实例的方法
func New1() *viper.Viper { return viper.New() } func New2() *viper.Viper { return viper.NewWithOptions() }
Sub为我们读取子配置项提供了一个新的实例Viper
v := viper.Sub("db") url := v.Get("url") log.Printf("mysql url:%s\n", url)
2.2 预设一些默认配置
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")
2.3 从命令行工具的选项参数Flags读取
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 ck@gmail.com
查看一下打印结果,可以看到从viper成功获取到以flag传入的参数值:
➜ cli git:(master) ✗ go run main.go --author keen --email ck@gmail.com go root cmd run ckeen ck@gmail.com
2.4 从环境变量读取
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)
使用方法:
使用AutomaticEnv()开启绑定环境变量,没开启的时候不会从环境变量获取,开启后可以获取环境变量的值。如果不想开启所有环境变量值,可以使用BindEnv(input …string)方法绑定单个环境变量的绑定, 那么只有该绑定的环境变量的key才能获取到值
绑定环境变量后,可以使用正常的Get方法来获取变量值,示例代码如下:
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")) }
还可以通过SetEnvPrefix()方法设置环境变量前缀, 前缀和Key之间用下划线分割
2.5 从配置文件读取
下面我们看一下操作实例, 先看我们的配置文件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"))
viper还提供了如下获取类型获取配置项值:
注: 其中重要的一个函数IsSet可以用来判断某个key是否被设置
2.6 从远程key/value存储读取
在Viper中启用远程支持,需要在代码中匿名导入viper/remote
这个包。
_ "github.com/spf13/viper/remote"
Viper将读取从Key/Value存储中的路径检索到的配置字符串(如JSON
、TOML
、YAML
格式)。viper目前支持Consul/Etcd/firestore三种Key/Value的存储系统。下面我来演示从etcd读取配置:
首先我们安装crypt的工具
go get github.com/bketelsen/crypt/bin/crypt
使用crypt的命令,将app.yml的文件添加到detcd
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
添加viper的操作远程资源的配置
_ "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")) }
测试打印结果
2.7 监听配置变化
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的值,然后保存后,我们可以看到控制台有打印最新修改后的值,不要我们重新去获取。
2.8 写入配置到文件
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", "ck@gmail.com") 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()方法,则直接传入配置文件路径即可。
3. 源码分析--配置读取的顺序
通过上面的示例我们知道,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统一转为了小写。
4. 参考资料
viper的包地址:viper package - github.com/spf13/viper - Go Packages
viper的github地址: GitHub - spf13/viper: Go configuration with fangs
总结
本篇关于《Go项目配置管理神器之viper的介绍与使用详解》的介绍就到此结束啦,但是学无止境,想要了解学习更多关于Golang的相关知识,请关注golang学习网公众号!
-
417 收藏
-
218 收藏
-
441 收藏
-
266 收藏
-
237 收藏
-
457 收藏
-
309 收藏
-
225 收藏
-
485 收藏
-
233 收藏
-
322 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习