1.2. viper 配置读取

未匹配的标注

引言

viper 是一个用于读取配置文件的库。如果你需要读取配置文件,那么 viper 足够好用。

项目地址

项目地址: 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

实例代码

github.com/oscome/godaily/tree/mai...

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 里面的代码还是有很多值得参考的,我觉得感兴趣可以深入看一下。


关注和赞赏都是对笔者最大的支持
关注和赞赏都是对笔者最大的支持

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 查看所有版本


暂无话题~