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,其灵活的配置选项可以更好地实现容器化部署。并且可以很好适应多种应用场景,让应用程序摆脱配置的束缚。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容