配置文件读取是很多Go项目必备的功能,这方面社区提供的方案也相对成熟稳定。但之前写这部分代码时除了使用了针对不同配置文件格式(比如:ini、toml等)的驱动包之外,很少直接使用第三方包对读取出的配置项的值进行管理。于是我们就面对这样一个问题:其他包如果要使用这些被读取出的配置项的值该如何做呢?我们以读取ini格式承载的配置文件为例,来简单说说。

1. 全局变量法

这是最粗糙的方法,但却是最易理解的方法。我们建立一个config包,在main函数中读取配置文件并将读取到的配置项信息存放在config包的一个导出的全局变量中,这样其他包要想获取配置文件中配置项的信息,直接通过该全局变量读取即可。下面的demo1就是一个使用全局变量组织读取出的配置项信息的示例项目,其代码结构如下:

// github.com/bigwhite/experiments/tree/master/read-ini/demo1demo1
├── conf
│   └── demo.ini
├── go.mod
├── go.sum
├── main.go
└── pkg├── config│   └── config.go└── pkg1└── pkg1.go

demo1中的conf/demo.ini中存储了下面这些配置项信息:

$cat demo.ini
[server]
id = 100001
port = 23333
tls_port = 83333[log]
level = 0; info:0, warn: 1, error: 2, dpanic:3, panic:4, fatal: 5, debug: -1
compress = true ; indicate whether the rotated log files should be compressed using gzip (default true)
path = "./log/demo.log" ; if it is empty, we use default logger(to stderr)
max_age = 3 ; the maximum number of days to retain old log files based on the timestamp encoded in their filename
maxbackups = 7 ; the maximum number of old log files to retain (default 7)
maxsize = 100 ; the maximum size in megabytes of the log file before it gets rotated (default 100)[debug]
profile_on = true ;add profile web server for app to enable pprof through web
profile_port = 8091 ; profile web port

我们通过config包读取该配置文件(基于github.com/go-ini/ini包实现ini配置文件读取):

// github.com/bigwhite/experiments/tree/master/read-ini/demo1/pkg/config/config.go
package configimport (ini "github.com/go-ini/ini"
)type Server struct {Id      string `ini:""`Port    int    `ini:"port"`TlsPort int    `ini:"tls_port"`
}type Log struct {Compress   bool   `ini:"compress"`LogPath    string `ini:"path"`MaxAge     int    `ini:"max_age"`MaxBackups int    `ini:"maxbackups"`MaxSize    int    `ini:"maxsize"`
}type Debug struct {ProfileOn   bool   `ini:"profile_on"`ProfilePort string `ini:"profile_port"`
}type IniConfig struct {Server `ini:"server"`Log    `ini:"log"`Debug  `ini:"debug"`
}var Config = &IniConfig{}func InitFromFile(path string) error {cfg, err := ini.Load(path)if err != nil {return err}return cfg.MapTo(Config)
}

这是一种典型的Go通过struct field tag与ini配置文件中p和key绑定读取的示例,我们在main包中调用InitFromFile读取ini配置文件:

// github.com/bigwhite/experiments/tree/master/read-ini/demo1/main.gopackage mainimport ("github.com/bigwhite/readini/pkg/config""github.com/bigwhite/readini/pkg/pkg1"
)func main() {err := config.InitFromFile("conf/demo.ini")if err != nil {panic(err)}pkg1.Foo()
}

读取后的配置项信息存储在config.Config这个全局变量中。在其他包中(比如pkg/pkg1/pkg1.go),我们可直接访问该全局变量获取配置项信息:

// github.com/bigwhite/experiments/tree/master/read-ini/demo1/pkg/pkg1/pkg1.go
package pkg1import ("fmt""github.com/bigwhite/readini/pkg/config"
)func Foo() {fmt.Printf("%#v\n", config.Config)
}

这种方式很简单、直观也易于理解,但以全局变量形式将配置项信息暴露给其他包,从代码设计层面,这总是会予人口实的。那么我们是否可以只暴露包函数,而不暴露具体实现呢?

2. 通过p.key形式读取配置项

由于是采用的tag与结构体字段的绑定方法,实际配置项名字与绑定的字段名字可能是不一致的,比如下面代码段中的结构体字段TlsPort与其tag tls_port:

type Server struct {Id      string `ini:"id"`Port    int    `ini:"port"`TlsPort int    `ini:"tls_port"`
}

这样使用config包的用户在要获取配置项值时就必须了解绑定的结构体字段的名字。如果我们不暴露这些绑定结构体的实现细节的话,config包的用户所掌握的信息仅仅就是配置文件(比如:demo.ini)本身了。

于是一个很自然的想法就会萌发出来!我们是否可以通过p.key的形式得到对应配置项的值,比如以下面配置项为例:

[server]
id = 100001
port = 23333
tls_port = 83333

我们需要通过server.id来获得id这个配置项的值,类似的其他配置项的获取方式是传入server.port、server.tls_port等。这样我们的config包仅需保留类似一个接收xx.yy.zz为参数的GetSectionKey函数即可,就像下面这样:

id, ok := config.GetSectionKey("server.id")

接下来,我们就沿着这个思路在demo1的基础上重构为新方案demo2。下面是修改后的demo2的config包代码:

// github.com/bigwhite/experiments/tree/master/read-ini/demo2/pkg/config/config.go
package configimport ("reflect""strings"ini "github.com/go-ini/ini"
)type server struct {Id      string `ini:"id"`Port    int    `ini:"port"`TlsPort int    `ini:"tls_port"`
}type log struct {Compress   bool   `ini:"compress"`LogPath    string `ini:"path"`MaxAge     int    `ini:"max_age"`MaxBackups int    `ini:"maxbackups"`MaxSize    int    `ini:"maxsize"`
}type debug struct {ProfileOn   bool   `ini:"profile_on"`ProfilePort string `ini:"profile_port"`
}type iniConfig struct {Server server `ini:"server"`Log    log    `ini:"log"`Dbg    debug  `ini:"debug"`
}var thisConfig = iniConfig{}func InitFromFile(path string) error {cfg, err := ini.Load(path)if err != nil {return err}return cfg.MapTo(&thisConfig)
}func GetSectionKey(name string) (interface{}, bool) {keys := strings.Split(name, ".")lastKey := keys[len(keys)-1]v := reflect.ValueOf(thisConfig)t := reflect.TypeOf(thisConfig)found := falsefor _, key := range keys {cnt := v.NumField()for i := 0; i < cnt; i++ {field := t.Field(i)if field.Tag.Get("ini") == key {t = field.Typev = v.Field(i)if key == lastKey {found = true}break}}}if found {return v.Interface(), true}return nil, false
}

我们将原先暴露出去的全局变量改为了包内变量(thisConfig),几个绑定的结构体类型也都改为非导出的了。我们提供了一个对外的函数:GetSectionKey,这样通过该函数,我们就可以使用p.key的形式获取到对应配置项的值了。在GetSectionKey函数的实现中,我们使用了反射来获取结构体定义中各个字段的tag来和传入的p.key的各个部分做比对,一旦匹配,便将对应的值传出来。如果没有匹配到,则返回false,这里GetSectionKey的返回值列表设计也使用了经典的“comma, ok”模式。

这样,我们在pkg1包中便可以这样来获取对应的配置项的值了:

// github.com/bigwhite/experiments/tree/master/read-ini/demo2/pkg/pkg1/pkg1.go
package pkg1import ("fmt""github.com/bigwhite/readini/pkg/config"
)func Foo() {id, ok := config.GetSectionKey("server.id")fmt.Printf("id = [%v], ok = [%t]\n", id, ok)tlsPort, ok := config.GetSectionKey("server.tls_port")fmt.Printf("tls_port = [%v], ok = [%t]\n", tlsPort, ok)logPath, ok := config.GetSectionKey("log.path")fmt.Printf("path = [%v], ok = [%t]\n", logPath, ok)logPath1, ok := config.GetSectionKey("log.path1")fmt.Printf("path1 = [%v], ok = [%t]\n", logPath1, ok)
}

运行demo2,我们将看到如下结果:

$go run main.go
id = [100001], ok = [true]
tls_port = [83333], ok = [true]
path = [./log/demo.log], ok = [true]
path1 = [<nil>], ok = [false]

现在还有一个问题,那就是config包暴露的函数GetSectionKey的第一个返回值类型为interface{},这样我们得到配置项的值后还得根据其类型通过类型断言方式进行转型,体验略差,我们可以在config包中提供常见类型的“语法糖”函数,比如下面这些:

// github.com/bigwhite/experiments/tree/master/read-ini/demo2/pkg/config/config.go
func GetInt(name string) (int, bool) {i, ok := GetSectionKey(name)if !ok {return 0, false}if v, ok := i.(int); ok {return v, true}// maybe it is a digital strings, ok := i.(string)if !ok {return 0, false}n, err := strconv.Atoi(s)if err != nil {return 0, false}return n, true
}func GetString(name string) (string, bool) {i, ok := GetSectionKey(name)if !ok {return "", false}s, ok := i.(string)if !ok {return "", false}return s, true
}func GetBool(name string) (bool, bool) {i, ok := GetSectionKey(name)if !ok {return false, false}b, ok := i.(bool)if !ok {return false, false}return b, true
}

这样我们在pkg1包中就可以直接使用这些语法糖函数获取对应类型的配置项值了:

// github.com/bigwhite/experiments/tree/master/read-ini/demo2/pkg/pkg1/pkg1.go
b, ok := config.GetBool("debug.profile_on")
fmt.Printf("profile_on = [%t], ok = [%t]\n", b, ok)

3. 优化

配置读取一般都是在系统初始化阶段,对其性能要求不高。后续系统运行过程中,也会偶有获取配置项的业务逻辑。一旦在关键路径上有获取配置项值的逻辑,上面的方案便值得商榷,因为每次通过GetSectionKey获取一个配置项的值都要通过反射进行一番操作,性能肯定不佳。

那么如何优化呢?我们可以通过为每个key建立索引来进行。我们在config包中创建一个除初始化时只读的map变量:

// github.com/bigwhite/experiments/tree/master/read-ini/demo3/pkg/config/config.go
var index = make(map[string]interface{}, 100)

在config包的InitFromFile中我们将配置项以p.key为key的形式索引到该index变量中:

// github.com/bigwhite/experiments/tree/master/read-ini/demo3/pkg/config/config.gofunc InitFromFile(path string) error {cfg, err := ini.Load(path)if err != nil {return err}err = cfg.MapTo(&thisConfig)if err != nil {return err}createIndex()return nil
}

createIndex的实现如下:

// github.com/bigwhite/experiments/tree/master/read-ini/demo3/pkg/config/config.go
func createIndex() {v := reflect.ValueOf(thisConfig)t := reflect.TypeOf(thisConfig)cnt := v.NumField()for i := 0; i < cnt; i++ {fieldVal := v.Field(i)if fieldVal.Kind() != reflect.Struct {continue}// it is a struct kind field, go on to get tagfieldStructTyp := t.Field(i)tag := fieldStructTyp.Tag.Get("ini")if tag == "" {continue // no ini tag, ignore it}// append Field RecursivelyappendField(tag, fieldVal)}
}func appendField(parentTag string, v reflect.Value) {cnt := v.NumField()for i := 0; i < cnt; i++ {fieldVal := v.Field(i)fieldTyp := v.Type()fieldStructTyp := fieldTyp.Field(i)tag := fieldStructTyp.Tag.Get("ini")if tag == "" {continue}if fieldVal.Kind() != reflect.Struct {// leaf field,  add to mapindex[parentTag+"."+tag] = fieldVal.Interface()} else {// recursive call appendFieldappendField(parentTag+"."+tag, fieldVal)}}
}

这样我们的GetSectionKey就会变得异常简单:

func GetSectionKey(name string) (interface{}, bool) {v, ok := index[name]return v, ok
}

我们看到:每次调用config.GetSectionKey将变成一次map的查询操作,这性能那是相当的高:)。

4. 第三方方案

其实前面那些仅仅是一个配置项读取思路的演进过程,你完全无需自行实现,因为我们有实现的更好的第三方包可以直接使用,比如viper[2]。我们用viper来替换demo3中的config包,代码见demo4:

// github.com/bigwhite/experiments/tree/master/read-ini/demo4/main.go
package mainimport ("github.com/bigwhite/readini/pkg/pkg1""github.com/spf13/viper"
)func main() {viper.SetConfigName("demo")viper.SetConfigType("ini")viper.AddConfigPath("./conf")err := viper.ReadInConfig()if err != nil {panic(err)}pkg1.Foo()
}

我们在main函数中利用viper的API读取demo.ini中的配置。然后在pkg1.Foo函数中向下面这样获取配置项的值即可:

// github.com/bigwhite/experiments/tree/master/read-ini/demo4/pkg/pkg1/pkg1.go
package pkg1import ("fmt""github.com/spf13/viper"
)func Foo() {id := viper.GetString("server.id")fmt.Printf("id = [%s]\n", id)tlsPort := viper.GetInt("server.tls_port")fmt.Printf("tls_port = [%d]\n", tlsPort)logPath := viper.GetString("log.path")fmt.Printf("path = [%s]\n", logPath)if viper.IsSet("log.path1") {logPath1 := viper.GetString("log.path1")fmt.Printf("path1 = [%s]\n", logPath1)} else {fmt.Printf("log.path1 is not found\n")}
}

上面的实现基本等价于我们在demo3中所作的一切,viper没有使用“comma, ok”模式,我们需要自己调用viper.IsSet来判断是否有某个配置项,而不是通过像GetString这样的函数返回的空字符串来判断。

使用viper后,我们甚至无需创建与配置文件中配置项对应的结构体类型了。viper是一个强大的Go配置操作框架,它能实现的不仅限于上面这些,它还支持写配置文件、监视配置文件变化并热加载、支持多种配置文件类型(JSON, TOML, YAML, HCL, ini等)、支持从环境变量和命令行参数读取配置,并且命令行参数、环境变量、配置文件等究竟以哪个配置为准,viper是按一定优先级次序的,从高到低分别为:

  • 明确调用Set

  • flag

  • env

  • config

  • key/value store

  • default

有如此完善的配置操作第三方库,我们完全无需手动撸自己的实现了。

5. 小结

除了在本文中提供的使用包级API获取配置项值的方法外,我们还可以将读取出的配置项集合放入应用上下文,以参数的形式“传递”到应用的各个角落,但笔者更喜欢向viper这种通过公共函数获取配置项的方法。本文阐述的就是这种思路的演化过程,并给出一个“玩票”的实现(未经系统测试),以帮助大家了解其中原理,但不要将其用到你的项目中哦。

本文涉及的源码请到这里下载[3]:https://github.com/bigwhite/experiments/tree/master/read-ini。


“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!部落目前虽小,但持续力很强。在2021年上半年,部落将策划两个专题系列分享,并且是部落独享哦:

  • Go技术书籍的书摘和读书体会系列

  • Go与eBPF系列

欢迎大家加入!

Go技术专栏“改善Go语⾔编程质量的50个有效实践[4]”正在慕课网火热热销中!本专栏主要满足广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订 阅!


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用[5]”在慕课网热卖中,欢迎小伙伴们订阅学习!


我爱发短信[6]:企业级短信平台定制开发专家 https://51smspush.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展;短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址[7]:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 - https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx

  • 微信公众号:iamtonybai

  • 博客:tonybai.com

  • github: https://github.com/bigwhite

  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

参考资料

[1]

本文永久链接: https://tonybai.com/2021/07/10/read-ini-config-item-by-passing-p-key

[2]

viper: https://github.com/spf13/viper

[3]

这里下载: https://github.com/bigwhite/experiments/tree/master/read-ini

[4]

改善Go语⾔编程质量的50个有效实践: https://www.imooc.com/read/87

[5]

Kubernetes实战:高可用集群搭建、配置、运维与应用: https://coding.imooc.com/class/284.html

[6]

我爱发短信: https://51smspush.com/

[7]

链接地址: https://m.do.co/c/bff6eed92687

使用section.key的形式读取ini配置项相关推荐

  1. java inireader_用Java读取INI文件(带section的)

    代码 #include #include int main(){ boost::property_tree::ptree m_pt, tag_setting; read_ini("confi ...

  2. QSettings读取ini文件,中文key方法

    (吐槽:什么鬼,为啥要用中文当key???我也不知道呀) QSettings读取ini文件,其中key是中文的.例如: [单位] 1=运维检修部1(检修公司) 2=运维检修部2(检修公司)[班组] 1 ...

  3. VB读取INI配置文件各方资料整合

    VB读写ini文件(1) 自从注册表诞生以来ini文件正在逐渐失去其市场占有率,然而基于ini文件的独立性,致使其还没有到达退出历史舞台的地步,很多应用程序的初始化和一些界面参数的设置仍然很愿意从in ...

  4. golang 读取 ini配置信息

    package main //BY: 29295842@qq.com //这个有一定问题   如果配置信息里有中文就不行 //[Server] ;MYSQL配置 //Server=localhost  ...

  5. C# 读取ini文件 百度问问学习文档

    C# 读取ini文件 10 有多个section,现想读取整个ini文件和指定section下所有内容 补充: 发布答案可以,请对准题目啊,我不要指定节点的内容,我知道!我要的是读取指定区域的内容,假 ...

  6. 【自动化测试】Python 读取 .ini 格式文件

    大家应该接触过.ini格式的配置文件.配置文件就是把一些配置相关信息提取出去来进行单独管理,如果以后有变动只需改配置文件,无需修改代码.特别是后续做自动化的测试,需要拎出一部分配置信息,进行管理.比如 ...

  7. c读取ini配置文件_Go-INI - 超赞的Go语言INI文件操作库

    INI 文件(Initialization File)是十分常用的配置文件格式,其由节(section).键(key)和值(value)组成,编写方便,表达性强,并能实现基本的配置分组功能,被各类软件 ...

  8. Linux 脚本修改ini,Shell脚本读取ini配置文件的实现代码2例

    一.简单版 参考stackoverflow的例子,改了一个出来: while IFS='= ' read var val do if [[ $var == \[*] ]] then section=$ ...

  9. python ini文件 遍历_【自动化测试】Python 读取 .ini 格式文件

    大家应该接触过.ini格式的配置文件.配置文件就是把一些配置相关信息提取出去来进行单独管理,如果以后有变动只需改配置文件,无需修改代码.特别是后续做自动化的测试,需要拎出一部分配置信息,进行管理.比如 ...

最新文章

  1. python支持的数据类型list_Python基本数据类型之list
  2. (转)NGUI研究院之三种方式监听NGUI的事件方法
  3. Oracle Varchar2 字段转成Clob字段
  4. css:构建响应式网站
  5. 解决安装XMind出现Invalid Configuration Location The configuration area at ‘C:\Users\Administrator\Applicat
  6. cx_oracle写日志信息_看日志痛苦——可能是你方法不对
  7. 普通电阻触摸屏多点触摸低成本解决方 转载
  8. matlab dll没有头文件,matlab调用dll没有头文件怎么办
  9. JVM内存管理:深入垃圾收集器与内存分配策略
  10. 模板 - 数论 - 整除分块
  11. 剑指offer面试题[41]-和为s的两个数VS和为s的连续正数序列
  12. 哈夫曼编码+python实现
  13. 力扣-119. 杨辉三角 II
  14. windows桌面股票价格软件
  15. exchange 2010高可用平台的搭建和配置
  16. 32位计算机64位计算机实质,4G内存电脑安装32位还是64位win7系统?实际详细性能测试!...
  17. 最难学的七大编程语言,VB 第一,Python垫底,看你学的排第几
  18. 在vue新版脚手架中下载和使用jquery
  19. 无线网络技术导论笔记(第三讲)
  20. 玲珑杯-射击气球-点到线段的距离

热门文章

  1. 极大似然估计——简述
  2. 极大似然估计法的理解和用途
  3. python爬取二次元肥宅最喜欢的壁纸图片,看过的都说爱了
  4. 2022-2028年中国LCD(液晶显示器)行业市场发展现状及投资策略研究报告
  5. P2615 [NOIP2015 提高组] 神奇的幻方
  6. Springboot旅游管理系统 08841计算机毕业设计-课程设计-期末作业-毕设程序代做
  7. python2编码unicode,在不终止Unicode的情况下,在Python 2中编码转义字符的正确方法是什么?...
  8. 计算机系统的组成一般不包括,计算机系统的基本组成,一般应包括哪些
  9. 计算机主板现状及发展趋势论文,2020年计算机主板发展趋势分析 2020-2026年全球与中国计算机主板市场深度调研与发展趋势分析...
  10. 什么是运维?运维开发需要做哪些事情?