viper

github.com/spf13/viper: 读取配置文件

参考链接:
https://github.com/spf13/viper
https://juejin.cn/post/6844904051369312264

优先级

explicit call to Set
flag
env
config
key/value store
default

直接设置环境

viper.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})

读取配置文件

支持配置文件格式:JSON, TOML, YAML, HCL, INI, envfile and Java Properties files

注意:viper的配置的键值是大小写不敏感的,对环境变量是大小写敏感的。一个viper实例支持多个路径环境(但是至少得提供一个),但是只支持解析1个配置文件

另外注意,在写gitleaks打包项目时注意到,viper在进行配置时可以配置路径、原生的配置文件、后缀名等等,但是读取的行为基本上时这两个函数:
Eg:

1. viper.ReadConfig: # 读取raw的配置文件本身
bindata_default_toml, _ := bindata.Asset("gitleaks-n-all-kill.toml")
viper.SetConfigType("toml")
if err := viper.ReadConfig(bytes.NewBuffer(bindata_default_toml)); err != nil {
    log.Fatal().Msgf("unable to load gitleaks config, err: %s", err)
}
2. viper.ReadInConfig() # viper.ReadInConfig()读取某个配置文件
viper.SetConfigFile(cfgPath)          
fmt.Print("已加载config参数")
if err := viper.ReadInConfig(); err != nil {
    log.Fatal().Msgf("unable to load gitleaks config, err: %s", err)
}

viper.SetConfigName("config") // name of config file (without extension)
viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name
viper.AddConfigPath("/etc/appname/")   // path to look for the config file in
viper.AddConfigPath("$HOME/.appname")  // call multiple times to add many search paths
viper.AddConfigPath(".")               // optionally look for config in the working directory
err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file
    panic(fmt.Errorf("fatal error config file: %w", err))
}

if err := viper.ReadInConfig(); err != nil {
    if _, ok := err.(viper.ConfigFileNotFoundError); ok {
        // Config file not found; ignore error if desired
    } else {
        // Config file was found but another error was produced
    }
}
// Config file found and successfully parsed

另一个案例

package main 
import ( "fmt" "log" "github.com/spf13/viper" ) 
func main() 
{     viper.SetConfigName("config") 
viper.SetConfigType("toml") 
viper.AddConfigPath(".") 

//viper.ReadInConfig() 之前,可以先设置个默认值,配置文件此时还没解析,解析出来他自动覆盖这个默认值
viper.SetDefault("redis.port", 6381)

err := viper.ReadInConfig() 
if err != nil { log.Fatal("read config failed: %v", err) } 
fmt.Println(viper.Get("app_name")) 
fmt.Println(viper.Get("log_level")) 
fmt.Println("mysql ip: ", viper.Get("mysql.ip")) 
fmt.Println("mysql port: ", viper.Get("mysql.port")) 
fmt.Println("mysql user: ", viper.Get("mysql.user")) 
fmt.Println("mysql password: ", viper.Get("mysql.password")) 
fmt.Println("mysql database: ", viper.Get("mysql.database")) 
fmt.Println("redis ip: ", viper.Get("redis.ip")) 
fmt.Println("redis port: ", viper.Get("redis.port")) 
}

写配置文件

viper.WriteConfig() // writes current config to predefined path set by 'viper.AddConfigPath()' and 'viper.SetConfigName'
viper.SafeWriteConfig()
viper.WriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.config") // will error since it has already been written
viper.SafeWriteConfigAs("/path/to/my/.other_config")

Run time时盯配置文件是否有异动热加载配置。因此不需要重启服务器,就能让配置生效。

viper.OnConfigChange(func(e fsnotify.Event) {
    fmt.Println("Config file changed:", e.Name)
})
viper.WatchConfig()

如:
package main

import (
"fmt"
"log"
"time"

"github.com/spf13/viper"
)

func main() {
viper.SetConfigName("config")
viper.SetConfigType("toml")
viper.AddConfigPath(".")
err := viper.ReadInConfig()
if err != nil {
    log.Fatal("read config failed: %v", err)
}

viper.WatchConfig()

fmt.Println("redis port before sleep: ", viper.Get("redis.port"))
time.Sleep(time.Second * 10)
fmt.Println("redis port after sleep: ", viper.Get("redis.port"))
}

只需要调用viper.WatchConfig,viper 会自动监听配置修改。如果有修改,重新加载的配置。
上面程序中,我们先打印redis.port的值,然后Sleep 10s。在这期间修改配置中redis.port的值,Sleep结束后再次打印。

另外,还可以为配置修改增加一个回调:

viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Printf("Config file:%s Op:%s\n", e.Name, e.Op)
})

这样文件修改时会执行这个回调。

viper 使用fsnotify这个库来实现监听文件修改的功能。

gitleaks案例:读取[[Rule]]这种列表的toml数据

如这里结构体写的 Rules 是一个 []

type ViperConfig struct {
    Description string
    Extend      Extend
    Rules       []struct {
        ID          string
        Description string
        Entropy     float64
        SecretGroup int
        Regex       string
        Keywords    []string
        Path        string
        Tags        []string

        Allowlist struct {
            Regexes   []string
            Paths     []string
            Commits   []string
            StopWords []string
        }
    }
    Allowlist struct {
        Regexes   []string
        Paths     []string
        Commits   []string
        StopWords []string
    }
}

在toml中,其配置内容如下,是多个[[Rules]]这种写法

# [ GitLeaks原本的规则 ]

[[rules]]
description = "应用凭证--Adafruit API Key"
id = "adafruit-api-key"
regex = '''(?i)(?:adafruit)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9_-]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)'''
secretGroup = 1
keywords = [
    "adafruit",
]

[[rules]]
description = "应用凭证--Adobe Client ID (OAuth Web)"
id = "adobe-client-id"
regex = '''(?i)(?:adobe)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)'''
secretGroup = 1
keywords = [
    "adobe",
]

设置别名

viper.RegisterAlias("loud", "Verbose")
viper.Set("verbose", true) // same result as next line
viper.Set("loud", true)   // same result as prior line
viper.GetBool("loud") // true
viper.GetBool("verbose") // true

从io.Reader读取配置,例:读取bindata打包文件到viper

viper.SetConfigType("yaml") // or viper.SetConfigType("YAML")

// any approach to require this configuration into your program.
var yamlExample = []byte(`
Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
jacket: leather
trousers: denim
age: 35
eyes : brown
beard: true
`)

viper.ReadConfig(bytes.NewBuffer(yamlExample))

viper.Get("name") // this would be "steve"

绑定环境变量

func init() { 
// 绑定环境变量 
viper.BindEnv("redis.port") 
viper.BindEnv("go.path", "GOPATH") 
} 

func main() { 
// 省略部分代码 
fmt.Println("go path: ", viper.Get("go.path")) 
}

调用BindEnv方法,如果只传入一个参数,则这个参数既表示键名,又表示环境变量名。 如果传入两个参数,则第一个参数表示键名,第二个参数表示环境变量名。

还可以通过viper.SetEnvPrefix方法设置环境变量前缀,这样一来,通过AutomaticEnv和一个参数的BindEnv绑定的环境变量, 在使用Get的时候,viper 会自动加上这个前缀再从环境变量中查找。

如果对应的环境变量不存在,viper 会自动将键名全部转为大写再查找一次。所以,使用键名gopath也能读取环境变量GOPATH的值。

前缀的设置

SetEnvPrefix("spf") // will be uppercased automatically
BindEnv("id")

os.Setenv("SPF_ID", "13") // typically done outside of the app

id := Get("id") // 13

Unmarshal到结构体中

package main import ( "fmt" "log" "github.com/spf13/viper" ) 
type Config struct 
{ AppName string 
LogLevel string 
MySQL MySQLConfig 
Redis RedisConfig 
} 
type MySQLConfig struct { 
IP string 
Port int 
User string 
Password string 
Database string 
} 

type RedisConfig struct { IP string Port int } 

func main() { 
viper.SetConfigName("config") 
viper.SetConfigType("toml") 
viper.AddConfigPath(".") 
err := viper.ReadInConfig() 
if err != nil { 
log.Fatal("read config failed: %v", err) 
} 
var c Config 
viper.Unmarshal(&c) 
fmt.Println(c.MySQL) 
}

viper和cobra结合

serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))

viper和pflag结合

pflag.Int("flagname", 1234, "help message for flagname")
pflag.Parse()
viper.BindPFlags(pflag.CommandLine)

i := viper.GetInt("flagname") // retrieve values from viper instead of pflag

或者是:

func init() { 
pflag.Int("redis.port", 8381, "Redis port to connect") // 绑定命令行 
viper.BindPFlags(pflag.CommandLine) 
}

gitleaks案例:同viper要求读取2个配置文件,后者覆盖前者

此次案例参考链接: https://treexie.gitbook.io/articles/viper

背景: 要求打包gitleaks,把toml的默认配置文件嵌套进去,然后该默认的配置文件我们可控,且支持用户对其中某个单条的配置规则的修改或者添加。

参考链接中示例如下:

func initConfig() (err error) {
    configType := "yml"
    defaultPath := "./configs"
    v := viper.New()
    // 从default中读取默认的配置
    v.SetConfigName("default")
    v.AddConfigPath(defaultPath)
    v.SetConfigType(configType)
    err = v.ReadInConfig()
    if err != nil {
        return
    }

    configs := v.AllSettings()
    // 将default中的配置全部以默认配置写入
    for k, v := range configs {
        viper.SetDefault(k, v)
    } // 在这里使用到了viper.Set的方式设置值。 有Set()和SetDefault() 两个方法
    env := os.Getenv("GO_ENV")
    // 根据配置的env读取相应的配置信息
    if env != "" {
        viper.SetConfigName(env)
        viper.AddConfigPath(defaultPath)
        viper.SetConfigType(configType)
        err = viper.ReadInConfig()
// 这里继续重复进行读取
        if err != nil {
            return
        }
    }
    return
}

量来进行赋值传递

// 处理[allowlist] 开始
        if user_custom_configs["allowlist"] != nil {
            var tmpConfigAllowlist map[string]interface{}
            tmpConfigAllowlist = make(map[string]interface{})
            for k, v := range configs["allowlist"].(map[string]interface{}) {
                tmpConfigAllowlist[k] = v
                //fmt.Println(k)
                //fmt.Println(v)
            }
            user_custom_configs_allowlist := user_custom_configs["allowlist"].(map[string]interface{})
            for k, v := range user_custom_configs_allowlist {
                if k == "paths" {
                    tmpConfigAllowlist[k] = v
                }
            }
            configs["allowlist"] = tmpConfigAllowlist

        }

将viper中的toml读取到结构体

经过测试,标注的 Description string toml:"description"
此处最好不要写toml,而是写成 mapstructure:"advice",会少一些读取不到的奇奇怪怪的错误

output.toml

# gitlab version with risk
[version]

# admin setting
[settings]
[settings.password]
check_rule="密码复杂度检测"
check_rule_en="password_complex"
description="密码复杂度检测"
advice="密码复杂度检测"
[settings.password.length]
check_rule="最小长度"
check_rule_en="password_least_length"
description="最小长度"
advice="要求数字"

写的结构体

type Output struct {
    Settings struct {
        Password struct {
            CheckRule   string `mapstructure:"check_rule"`
            CheckRuleEn string `mapstructure:"check_rule_en"`
            Description string `mapstructure:"description"`
            Advice      string `mapstructure:"advice"`
            Length      struct {
                CheckRule   string `mapstructure:"check_rule"`
                Description string `mapstructure:"description"`
                Advice      string `mapstructure:"advice"`
            } `toml:"length"`
            Num struct {
                CheckRule   string `mapstructure:"check_rule"`
                Description string `mapstructure:"description"`
                Advice      string `mapstructure:"advice"`
            } `toml:"num"`
            Upper struct {
                CheckRule   string `mapstructure:"check_rule"`
                Description string `mapstructure:"description"`
                Advice      string `mapstructure:"advice"`
            } `toml:"upper"`
            Lower struct {
                CheckRule   string `mapstructure:"check_rule"`
                Description string `toml:"description"`
                Advice      string `toml:"advice"`
            } `toml:"lower"`
            Special struct {
                CheckRule   string `toml:"check_rule"`
                Description string `toml:"description"`
                Advice      string `toml:"advice"`
            } `toml:"special"`
        } `toml:"password"`
    } `toml:"settings"`
}

viper读取配置文件后读取到结构体中

func (o *Output) GetDefault() {
    outputConfig := viper.New()
    bindataOutputDefaultToml, _ := bindata.Asset("output.toml")
    outputConfig.SetConfigType("toml")
    if err := outputConfig.ReadConfig(bytes.NewBuffer(bindataOutputDefaultToml)); err != nil {
        panic("unable to load output config")
        panic(err)
    }
    outputConfig.Unmarshal(o)
}

Welcome to point out the mistakes and faults!