day2-viper 配置读取

引言

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)
}

效果如图: viper

实例代码

https://github.com/oscome/godaily/tree/main/day002

tips

  1. viper 读取优先级是 Set 方法、flag、env、config、k/v、默认值
  2. viper 配置键不区分大小写
  3. 除了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 里面的代码还是有很多值得参考的,我觉得感兴趣可以深入看一下。

关注和赞赏都是对小欧莫大的支持! 🤝 🤝 🤝
公众号