Go项目配置管理神器之viper的介绍与使用详解

目录
  • 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支持功能

  • 设置默认配置
  • 支持各种配置文件,如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存储中的路径检索到的配置字符串(如JSONTOMLYAML格式)。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的介绍与使用的文章就介绍到这了,更多相关Go配置管理神器viper使用内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Golang配置管理库 Viper的教程详解

    目录 一.Viper 是什么? 二.安装 Viper 三.Viper 有什么作用 四.Viper demo 可供参考 注意 五.总结 一.Viper 是什么? Viper 是应用程序的完整配置的管理工具,用于在应用程序中工作,可以处理所有类型的配置需求和格式. 二.安装 Viper go get github.com/spf13/viper 三.Viper 有什么作用 设置默认值 读取 JSON.TOML.YAML(YML).HCL.envfile 和 Java properties 属性配置文

  • Golang配置解析神器go viper使用详解

    目录 前言 viper简介 功能 viper配置优先级 安装viper 支持哪些文件格式 key大小写问题 使用指南 如何访问viper的功能 配置默认值 读取配置文件 写配置文件 WriteConfig SafeWriteConfig WriteConfigAs SafeWriteConfigAs 监听配置文件 从io.Reader读取配置 显示设置配置项 注册和使用别名 读取环境变量 与命令行参数搭配使用 pflag 扩展其他flag 远程key/value存储支持 访问配置 直接访问 序列

  • Python神器之Pampy模式匹配库的用法详解

    目录 Pampy 是哪路神仙 Pampy 的花式秀 匹配单个字符 匹配字典 匹配开头和结尾 总结 大家好,我是闲欢,一个很卷的程序员! 今天给大家分享一个炒鸡炒鸡简单又好用的神器——pampy. 我敢以我的荣誉保证,用了它之后,你写代码的效率可以蹭蹭蹭地提升! Pampy 是哪路神仙 首先普及一下模式匹配. 模式匹配即给定某种模式,用这种模式去检查序列或字符串是否符合这种模式,这种技术在自然语言处理中经常使用. Pampy 是 Python 的一个模式匹配类库,一个只有150行的类库,该库优雅.

  • Eclipse中导入Maven Web项目并配置其在Tomcat中运行图文详解

    今天因为实习的关系需要讲公司已经开发的项目导入进Eclipse,而公司的项目是用Maven来构建的所以,需要将Maven项目导入进Eclipse下. 自己因为没有什么经验所以搞了得两个多小时,在这里和大家分享一下自己的经验已经在这之中遇到的一些问题. 首先我通过svn将公司的项目checkout到了本地. 因为Maven遵循的是规约比配置重要的原则,所以Maven项目的结构一般是进入目录后是一个pom.xml文件和一个src文件夹,当然可能还存在一些README之类的这些都不重要,最关键的就是p

  • 在vue项目中优雅的使用SVG的方法实例详解

    1.基础介绍 本文旨在介绍如何在项目中配置和方便的使用svg图标. 本文以vue项目为例,当然在react中的使用原理基本相似. svg图标可以直接通过img标签来使用,也可当做icon来使用. 本文是参考了鑫旭大佬的文章:SVG Sprite技术介绍. 2.配置 安装svg-sprite-loader.通过vue-cli脚手架创建的项目默认情况下会使用 url-loader 对svg进行处理,所以需要处理下: { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, l

  • Maven build 命令介绍的使用详解

    常用命令: 打包:mvn package 编译:mvn compile 清空:mvn clean(清除编译后目录,默认是target目录) 运行测试:mvn test 安装jar包到本地仓库中:mvn install 跳过测试:mvn xxx -DskipTests 1.创建 Maven 工程 ①.在 src/main/java 新建包 com.ys.maven,然后在这个包中创建类 HelloMaven.java package com.ys.maven; public class Hello

  • IntelliJ IDEA 的 Spring 项目如何查看 @Value 的配置和值(方法详解)

    当你打开项目或者项目中的文件的时候,如果你有 Spring 的 Value 的配置,Intellij 将会自动将参数替换为值. 如果你单击上面的值,那么这个配置参数将会显示为配置的参数名. 如果你还想显示值的话,你需要重新打开这个文件或者项目. 有没有什么快捷键可以快速进行切换. 快捷键 这个配置是在 Intellij 的 Code > Folding 中进行配置的. 快捷键是是 Ctrl + NumberPad + 快捷键是是 Ctrl + NumberPad - NumberPad +,这个

  • Python JWT 介绍和使用详解

    1. JWT 介绍 ​jwt( JSON Web Tokens ),是一种开发的行业标准 RFC 7519 ,用于安全的表示双方之间的声明.目前,jwt广泛应用在系统的用户认证方面,特别是现在前后端分离项目 ​jwt认证流程: ​在项目开发中,一般会按照上图所示的过程进行认证,即:用户登录成功之后,服务端给用户浏览器返回一个 token,以后用户浏览器要携带 token 再去向服务端发送请求,服务端校验 token 的合法性,合法则给用户看数据,否则,返回一些错误信息 ​传统token方式和jw

  • React Fragment介绍与使用详解

    目录 前言 Fragments出现动机 React Fragment介绍与使用 <React.Fragment> 与 <>区别 前言 在向 DOM 树批量添加元素时,一个好的实践是创建一个document.createDocumentFragment,先将元素批量添加到 DocumentFragment 上,再把 DocumentFragment 添加到 DOM 树,减少了 DOM操作次数的同时也不会创建一个新元素. 和 DocumentFragment 类似,React 也存在

  • TKMybatis的介绍和使用详解

    目录 一.什么是 TKMybatis 二.TKMybatis 使用 2.1 Springboot 项目中加入依赖 2.2 使用讲解 2.3 实际案例 三.扩展介绍 泛型(实体类)的类型必须符合要求 所有的mapper继承此类将具有以下通用方法 一.什么是 TKMybatis TKMybatis 是基于 Mybatis 框架开发的一个工具,内部实现了对单表的基本数据操作,只需要简单继承 TKMybatis 提供的接口,就能够实现无需编写任何 sql 即能完成单表操作. 二.TKMybatis 使用

  • SpringBoot搭建Dubbo项目实现斐波那契第n项详解

    目录 step1 新建项目 step2 新建需要的包和接口以及实现类 step3 在两个项目的resource下新建配置文件 step4 代码编写 导入依赖 provider consumer 端口冲突更改 step5 运行 step1 新建项目 方法1:直接在IDEA里新建如图: 方法2:在start.spring.io新建 可能有的小朋友已经发现了,第一种方式的Server URL就是第二个的网站,都是一样的 要新建两个项目,第一个项目如上图所示,第二个项目只需要将provider改为con

  • Vue中mixins混入的介绍与使用详解

    目录 一.来自官网的描述 二.如何创建Mixins 三.项目中如何使用混入 四.与vuex的区别 五.与公共组件的区别 一.来自官网的描述 混入 (mixins): 是一种分发 Vue 组件中可复用功能的非常灵活的方式.混入对象可以包含任意组件选项.当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项. 二.如何创建Mixins 在src目录下创建一个mixins文件夹,文件夹下新建一个myMixins.js文件.前面我们说了mixins是一个js对象,所以应该以对象的形式来定义my

随机推荐