spf13/viper——Go应用程序的完整配置解决方案

一、viper的简单介绍

1.viper支持的功能:

1、可以设置默认值
2、可以加载多种格式的配置文件,如JSON,TOML,YAML,HCL和Java属性配置文件
3、应用程序运行过程中,保持监听和重新读取配置文件
4、可以从环境变量读取配置
5、可以从远程配置系统读取配置
6、可以读取命令行标志作为配置
7、可以从缓冲区中读取
8、设置显式的值

  • 在GitHub中,作者是这样描述viper对于开发人员的作用:在构建现代化应用程序的过程中,开发人员可以通过使用viper而不必考虑配置文件的格式问题。

2.viper具体的帮助如下:

1、可以查找、加载和反序列化多种格式的配置文件,如JSON, TOML, YAML, HCL, Java属性配置格式。
2、提供一种为不同配置选项设置默认值的机制
3、提供一种通过命令行标志覆盖指定配置选项值的机制
4、提供了一种别名系统,可以在避免破坏现有代码的前提下,轻松地重命名参数
5、当用户提供的命令行或配置文件的配置选项与默认的配置选项相同时,可以很容易通过选项值结果看出优先级的差异。

3.viper提供的配置方式的优先级顺序如下(由高到低):

1.设置显示调用(explicit call to Set)
2.命令行标志(flag)
3.环境变量(env)
4.配置文件(config)
5.远程键/值存储(key/value store)
6.默认值(default)

viper的简单使用

二、viper的简单使用

就我个人理解,应用程序的源代码里写的配置选项值的优先级应该低一些,如果太高会很不灵活(除非有些配置是涉及安全等方面,而钻牛角尖地说,这样的参数又不必穿上配置选项的马甲了~~)。所以我的理解和目前的使用范围,我认为比较常用的是flagenvconfigdefault,并且基本够用了。下面也仅仅涉及这四种配置选项方式。
当启动一个应用程序的时候,用户通过命令行标志可以实现最高优先级的配置。而用户有些懒,不想写flag,同样不想改配置文件,但是又想实现一些动态可变的配置适配,那么可以考虑到环境变量配置选项,比如本地ip等值,可以通过相关api获取并设置为环境变量,绑定到指定的配置项。有些用户比较迟钝,将配置文件放在了错误的目录下,误删了配置文件,或者干脆忘记了配置文件这回事,怎么办呢?有一些基本不会更改的配置选项会通过设置默认值在应用程序中配置好,那么没有找到配置文件的时候,给到一个日志提醒就足够了,并不会影响应用程序的正常启动。
啰嗦了这么多,还是放码过来吧!

package main

import (
    flag "github.com/spf13/pflag"
    "fmt"
    "github.com/spf13/viper"
    "reflect"
    "os"
)

var ( // 命令行标志的定义
    kafkaBrokers = flag.StringArray("kb", []string{"192.168.0.0:9092","192.168.0.1:9092"}, "kafka brokers")
    conf       = flag.String("c", "doria.toml", "specify the configuration file, default is doria.toml")
    num        = flag.Int("n", 6,"specify the number")
)

func SetEnvFor() { // 设置环境变量
    os.Setenv("WINTER.NAME", "Bingham")
    os.Setenv("KAFKA.BROKERS", "192.168.1.1:9092 192.168.1.2:9092")
    os.Setenv("WINTER.AGE", "23")
}

func main() {
    flag.Parse()

    SetEnvFor()
    viper.AutomaticEnv()
    viper.BindEnv("f", "winter.Name") // 绑定环境变量
    viper.BindEnv("f", "kafka.Brokers")

    //flag.PrintDefaults()
    viper.SetDefault("kafka.Brokers", "192.168.2.1:9092") // 设置默认值,最低优先级
    viper.SetDefault("winter.Age",16)
    viper.SetConfigName("doria")
    viper.AddConfigPath(".")
    err := viper.ReadInConfig()
    if err != nil {
        panic(fmt.Errorf("Fatal error config file: %s \n", err))
    }

    //err = viper.Unmarshal(v, optDecode)
    //if err != nil {
    //  panic(fmt.Errorf("Fatal error unmarshal : %s \n", err))
    //
    //}
    // flag取到其中函数设置默认值或者正常通过命令行标志获取值
    fmt.Printf("watch the value from kafkaBrokers : %s\n", *kafkaBrokers)
    fmt.Printf("watch the value from conf : %s\n", *conf)
    fmt.Printf("watch the value from num : %d\n", *num)
    fmt.Println("-----------------------------------------------")
    fmt.Printf("look the winter name is : %s\n", viper.GetString("winter.Name"))
    fmt.Printf("look the kafka brokers is : %v\n", viper.GetStringSlice("kafka.Brokers"))
    fmt.Printf("look the winter age is : %d\n", viper.GetInt("winter.Age"))

    viper.Set("kafka.Brokers","172.6.6.6:9092") // 最高优先级
    fmt.Printf("look the kafka brokers is : %v\n", viper.Get("kafka.Brokers"))
    fmt.Println("watch brokers` type : ", reflect.TypeOf( viper.Get("kafka.Brokers")))

    //fmt.Println(viper.Sub("kafka.Brokers"))
    
}

一些不太了解flag的小伙伴,这里需要稍稍提醒一下,运行这个代码的具体步骤如下(因为以前我什么都不懂的时候,运行网上的一些代码总会报错,以为是代码错了,其实是方式不对):

go build main.go
./main --kb="172.0.0.1:9092" --c="doria.yml" --n=3

这部分的代码涉及的内容是flagenvdefault,不用着急,config部分后面会提到。

三、viper关于config的讨论

还是先上代码吧。

package main

import (
    "github.com/spf13/viper"
    "fmt"
    flag "github.com/spf13/pflag"
    //goflag "flag"
    "github.com/c2h5oh/datasize"
    "github.com/mitchellh/mapstructure"
    "reflect"
)

//var ip *int = flag.Int("flagname", 1234, "help message for flagname")

type Config struct {
    Server Server
}

type Server struct {
    Id                 string
    Tcp_Bind           string
    DashboardBind      string
    MaxSize            *datasize.ByteSize
}

func main() {
    //var ip = flag.IntP(("flagname", "f", 1234, "help message")
    //flag.Lookup("flagname").NoOptDefVal = "4321"

    var ip = flag.String("flagname", "172.0.0.1", "help message for flagname")

    //goflag.CommandLine.AddGoFlagSet(goflag.CommandLine)
    flag.Parse()
    fmt.Println("ip has value ", *ip)

    viper.SetConfigName("config")  // 只有名字,没有后缀
    viper.AddConfigPath(".")
    err := viper.ReadInConfig()
    if err != nil {
        panic(fmt.Errorf("Fatal error config file: %s \n", err))
    }

    // 查看并重新读取配置文件
    //viper.WatchConfig()
    //viper.OnConfigChange(func(e fsnotify.Event) {
    //  fmt.Println("Config file changed:", e.Name)
    //})
    viper.Set("server.Tcp_Bind", *ip)
    fmt.Printf("watch the tcp_bind : %s\n", viper.Get("server.TcpBind"))
    fmt.Printf("watch the dash_bind : %s\n", viper.Get("server.DashboardBind"))

    var Cfg Config
    //fmt.Println("watch the viper is : ", viper.GetViper())

    optDecode := viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
        mapstructure.StringToTimeDurationHookFunc(),
        StringToByteSizesHookFunc(),
    ))

    err = viper.Unmarshal(&Cfg, optDecode)
    if err != nil {

    }

    //err = viper.Unmarshal(&Cfg)
    //if err != nil {
    //  fmt.Println("The error from Unmarshal is : ", err)
    //}
    fmt.Println("watch the config of Cfg is : ", Cfg)
    fmt.Printf("watch the config of MaxSize : %v\n", Cfg.Server.MaxSize)
}

func StringToByteSizesHookFunc() mapstructure.DecodeHookFunc {
    return func(
        f reflect.Type,
        t reflect.Type,
        data interface{}) (interface{}, error) {
        if f.Kind() != reflect.String {
            return data, nil
        }
        if t != reflect.TypeOf(datasize.ByteSize(5)) {
            return data, nil
        }

        // Convert it by parsing
        raw := data.(string)
        result := new(datasize.ByteSize)
        result.UnmarshalText([]byte(raw))
        return result.Bytes(), nil
    }
}

配置文件如下(吐槽一下,这里竟然不能识别toml格式的配置文件):

[server]
id = "1"
TcpBind = "172.11.22.33:6699"
DashboardBind = "172.11.22.33:6006"
MaxSize = "10m"

如果是一般提取配置文件内容,并且解析反序列化,倒也没什么说的。不过这里有这么一种情况。比如配置文件需要设置内存的最大占用量,设置为“10m” ,那么value就是字符串了,而在代码的结构中,对应的字段却是*datasize.ByteSize类型,实际是uint64类型,那么正常反序列化就会出错了。于是就需要用到反射的特性,mapstructure中有几种常用的转换绑定函数,例如

StringToSliceHookFunc()
StringToTimeDurationHookFunc()
StringToIPHookFunc()

而像我刚刚提到的场景是没有函数实现的,那么就需要自己去实现,就是上面的StringToByteSizesHookFunc()函数。

四、结语

目前涉及viper的使用大致就是这些了。在应用程序中使用到viper,其灵活的配置选项可以更好地实现容器化部署。并且可以很好适应多种应用场景,让应用程序摆脱配置的束缚。

你可能感兴趣的:(spf13/viper——Go应用程序的完整配置解决方案)