引言
viper 是一个用于读取配置文件的库。如果你需要读取配置文件,那么 viper 足够好用。
项目地址
项目地址: https://github.com/spf13/viper [star:20.7k]
使用场景
- 读取配置
安装
go get github.com/spf13/viper
常用方法
- SetConfigFile 定义配置文件
- ReadInConfig 读取配置文件
- GetString 获取某个key的配置
- WatchConfig 监听配置
- OnConfigChange 定义配置改变对应的操作
例子
我们可以增加一个文件
# oscome.yaml
name: oscome
mode: debug
log:
level: debug
我们可以使用 viper 读取这个配置文件,并且配合 fsnotify 监听配置,监听的好处就在于运行中配置几乎实时生效,无需重启服务。
package day002
import (
"fmt"
"testing"
"time"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
func read() {
viper.AddConfigPath(".") // 还可以在工作目录中查找配置
viper.SetConfigFile("oscome.yaml") // 指定配置文件路径(这一句跟下面两行合起来表达的是一个意思)
// viper.SetConfigName("oscome") // 配置文件名称(无扩展名)
// viper.SetConfigType("yaml") // 如果配置文件的名称中没有扩展名,则需要配置此项
err := viper.ReadInConfig() // 配置文件
if err != nil {
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}
}
func TestViper(t *testing.T) {
read()
t.Log(viper.GetString("name"))
t.Log(viper.GetString("log.level"))
}
func TestWatch(t *testing.T) {
read()
t.Log(viper.GetString("name"))
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
read()
t.Log(viper.GetString("name"))
t.Log(viper.GetString("log.level"))
})
time.Sleep(100 * time.Second)
}
效果如图:
实例代码
https://github.com/oscome/god...
tips
- viper 读取优先级是 Set 方法、flag、env、config、k/v、默认值
- viper 配置键不区分大小写
- 除了yaml,还支持 json、toml、ini等,新版本还支持 etcd,如果感兴趣,可以尝试一下。
源码解读
相对 cast 而言,viper 的代码要稍微复杂一点,我们重点看 ReadInConfig 和 WatchConfig。
ReadInConfig
func (v *Viper) ReadInConfig() error {
v.logger.Info("attempting to read in config file")
// 读取配置文件
filename, err := v.getConfigFile()
if err != nil {
return err
}
// 文件类型判断,支持 "json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "tfvars", "dotenv", "env", "ini"
if !stringInSlice(v.getConfigType(), SupportedExts) {
return UnsupportedConfigError(v.getConfigType())
}
v.logger.Debug("reading file", "file", filename)
// 涉及另一个库 afero,这里可以简单看成读取文件,返回 []byte 和 error
file, err := afero.ReadFile(v.fs, filename)
if err != nil {
return err
}
config := make(map[string]interface{})
// 解析文件内容
err = v.unmarshalReader(bytes.NewReader(file), config)
if err != nil {
return err
}
v.config = config
return nil
}
WatchConfig
func (v *Viper) WatchConfig() {
// 这里使用了 sync 包
initWG := sync.WaitGroup{}
initWG.Add(1)
go func() {
// fsnotify.NewWatcher()
watcher, err := newWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
filename, err := v.getConfigFile()
if err != nil {
log.Printf("error: %v\n", err)
initWG.Done()
return
}
configFile := filepath.Clean(filename)
// 获取配置文件所在目录,以便于后续监听
configDir, _ := filepath.Split(configFile)
realConfigFile, _ := filepath.EvalSymlinks(filename)
eventsWG := sync.WaitGroup{}
eventsWG.Add(1)
go func() {
for {
select {
case event, ok := <-watcher.Events:
// watcher.Events 这个通道关闭,注意 channel 两个返回值的写法哦
if !ok {
eventsWG.Done()
return
}
currentConfigFile, _ := filepath.EvalSymlinks(filename)
// 这里关心两种情况
// 1. 配置文件创建或修改
// 2. 配置文件的真实路径发生了变化(例如:k8s ConfigMap replacement)
const writeOrCreateMask = fsnotify.Write | fsnotify.Create
if (filepath.Clean(event.Name) == configFile &&
event.Op&writeOrCreateMask != 0) ||
(currentConfigFile != "" && currentConfigFile != realConfigFile) {
realConfigFile = currentConfigFile
err := v.ReadInConfig()
if err != nil {
log.Printf("error reading config file: %v\n", err)
}
// 调用自定义的 OnConfigChange
if v.onConfigChange != nil {
v.onConfigChange(event)
}
} else if filepath.Clean(event.Name) == configFile &&
event.Op&fsnotify.Remove != 0 {
eventsWG.Done()
return
}
case err, ok := <-watcher.Errors:
if ok { // 'Errors' channel is not closed
log.Printf("watcher error: %v\n", err)
}
eventsWG.Done()
return
}
}
}()
// 监听整个目录
watcher.Add(configDir)
initWG.Done()
// 等待下面一个 go routine 完成
eventsWG.Wait()
}()
// 确保上面的 go routine 在返回之前完全结束
initWG.Wait()
}
PS: viper 里面的代码还是有很多值得参考的,我觉得感兴趣可以深入看一下。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。