Go:使用section.key的形式读取ini配置项

发布于 2021-11-04 13:43 ,所属分类:软件编程学习资料

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

1. 全局变量法

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

//github.com/bigwhite/experiments/tree/master/read-ini/demo1

demo1
├──conf
│└──demo.ini
├──go.mod
├──go.sum
├──main.go
└──pkg
├──config
│└──config.go
└──pkg1
└──pkg1.go

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

$catdemo.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;indicatewhethertherotatedlogfilesshouldbecompressedusinggzip(defaulttrue)
path="./log/demo.log";ifitisempty,weusedefaultlogger(tostderr)
max_age=3;themaximumnumberofdaystoretainoldlogfilesbasedonthetimestampencodedintheirfilename
maxbackups=7;themaximumnumberofoldlogfilestoretain(default7)
maxsize=100;themaximumsizeinmegabytesofthelogfilebeforeitgetsrotated(default100)

[debug]
profile_on=true;addprofilewebserverforapptoenablepprofthroughweb
profile_port=8091;profilewebport

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

//github.com/bigwhite/experiments/tree/master/read-ini/demo1/pkg/config/config.go
packageconfig

import(
ini"github.com/go-ini/ini"
)

typeServerstruct{
Idstring`ini:""`
Portint`ini:"port"`
TlsPortint`ini:"tls_port"`
}

typeLogstruct{
Compressbool`ini:"compress"`
LogPathstring`ini:"path"`
MaxAgeint`ini:"max_age"`
MaxBackupsint`ini:"maxbackups"`
MaxSizeint`ini:"maxsize"`
}

typeDebugstruct{
ProfileOnbool`ini:"profile_on"`
ProfilePortstring`ini:"profile_port"`
}

typeIniConfigstruct{
Server`ini:"server"`
Log`ini:"log"`
Debug`ini:"debug"`
}

varConfig=&IniConfig{}

funcInitFromFile(pathstring)error{
cfg,err:=ini.Load(path)
iferr!=nil{
returnerr
}

returncfg.MapTo(Config)
}

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

//github.com/bigwhite/experiments/tree/master/read-ini/demo1/main.go

packagemain

import(
"github.com/bigwhite/readini/pkg/config"
"github.com/bigwhite/readini/pkg/pkg1"
)

funcmain(){
err:=config.InitFromFile("conf/demo.ini")
iferr!=nil{
panic(err)
}
pkg1.Foo()
}

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

//github.com/bigwhite/experiments/tree/master/read-ini/demo1/pkg/pkg1/pkg1.go
packagepkg1

import(
"fmt"

"github.com/bigwhite/readini/pkg/config"
)

funcFoo(){
fmt.Printf("%#v\n",config.Config)
}

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

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

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

typeServerstruct{
Idstring`ini:"id"`
Portint`ini:"port"`
TlsPortint`ini:"tls_port"`
}

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

于是一个很自然的想法就会萌发出来!我们是否可以通过section.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
packageconfig

import(
"reflect"
"strings"

ini"github.com/go-ini/ini"
)

typeserverstruct{
Idstring`ini:"id"`
Portint`ini:"port"`
TlsPortint`ini:"tls_port"`
}

typelogstruct{
Compressbool`ini:"compress"`
LogPathstring`ini:"path"`
MaxAgeint`ini:"max_age"`
MaxBackupsint`ini:"maxbackups"`
MaxSizeint`ini:"maxsize"`
}

typedebugstruct{
ProfileOnbool`ini:"profile_on"`
ProfilePortstring`ini:"profile_port"`
}

typeiniConfigstruct{
Serverserver`ini:"server"`
Loglog`ini:"log"`
Dbgdebug`ini:"debug"`
}

varthisConfig=iniConfig{}

funcInitFromFile(pathstring)error{
cfg,err:=ini.Load(path)
iferr!=nil{
returnerr
}

returncfg.MapTo(&thisConfig)
}

funcGetSectionKey(namestring)(interface{},bool){
keys:=strings.Split(name,".")
lastKey:=keys[len(keys)-1]
v:=reflect.ValueOf(thisConfig)
t:=reflect.TypeOf(thisConfig)

found:=false
for_,key:=rangekeys{
cnt:=v.NumField()

fori:=0;i<cnt;i++{
field:=t.Field(i)
iffield.Tag.Get("ini")==key{
t=field.Type
v=v.Field(i)
ifkey==lastKey{
found=true
}
break
}
}
}

iffound{
returnv.Interface(),true
}
returnnil,false
}

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

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

//github.com/bigwhite/experiments/tree/master/read-ini/demo2/pkg/pkg1/pkg1.go
packagepkg1

import(
"fmt"

"github.com/bigwhite/readini/pkg/config"
)

funcFoo(){
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,我们将看到如下结果:

$gorunmain.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
funcGetInt(namestring)(int,bool){
i,ok:=GetSectionKey(name)
if!ok{
return0,false
}

ifv,ok:=i.(int);ok{
returnv,true
}

//maybeitisadigitalstring
s,ok:=i.(string)
if!ok{
return0,false
}

n,err:=strconv.Atoi(s)
iferr!=nil{
return0,false
}
returnn,true
}

funcGetString(namestring)(string,bool){
i,ok:=GetSectionKey(name)
if!ok{
return"",false
}

s,ok:=i.(string)
if!ok{
return"",false
}
returns,true
}

funcGetBool(namestring)(bool,bool){
i,ok:=GetSectionKey(name)
if!ok{
returnfalse,false
}

b,ok:=i.(bool)
if!ok{
returnfalse,false
}
returnb,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
varindex=make(map[string]interface{},100)

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

//github.com/bigwhite/experiments/tree/master/read-ini/demo3/pkg/config/config.go

funcInitFromFile(pathstring)error{
cfg,err:=ini.Load(path)
iferr!=nil{
returnerr
}

err=cfg.MapTo(&thisConfig)
iferr!=nil{
returnerr
}

createIndex()
returnnil
}

createIndex的实现如下:

//github.com/bigwhite/experiments/tree/master/read-ini/demo3/pkg/config/config.go
funccreateIndex(){
v:=reflect.ValueOf(thisConfig)
t:=reflect.TypeOf(thisConfig)
cnt:=v.NumField()
fori:=0;i<cnt;i++{
fieldVal:=v.Field(i)
iffieldVal.Kind()!=reflect.Struct{
continue
}

//itisastructkindfield,goontogettag
fieldStructTyp:=t.Field(i)
tag:=fieldStructTyp.Tag.Get("ini")
iftag==""{
continue//noinitag,ignoreit
}

//appendFieldRecursively
appendField(tag,fieldVal)
}
}


funcappendField(parentTagstring,vreflect.Value){
cnt:=v.NumField()
fori:=0;i<cnt;i++{
fieldVal:=v.Field(i)
fieldTyp:=v.Type()
fieldStructTyp:=fieldTyp.Field(i)
tag:=fieldStructTyp.Tag.Get("ini")
iftag==""{
continue
}
iffieldVal.Kind()!=reflect.Struct{
//leaffield,addtomap
index[parentTag+"."+tag]=fieldVal.Interface()
}else{
//recursivecallappendField
appendField(parentTag+"."+tag,fieldVal)
}
}
}

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

funcGetSectionKey(namestring)(interface{},bool){
v,ok:=index[name]
returnv,ok
}

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

4. 第三方方案

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

//github.com/bigwhite/experiments/tree/master/read-ini/demo4/main.go
packagemain

import(
"github.com/bigwhite/readini/pkg/pkg1"
"github.com/spf13/viper"
)

funcmain(){
viper.SetConfigName("demo")
viper.SetConfigType("ini")
viper.AddConfigPath("./conf")
err:=viper.ReadInConfig()
iferr!=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
packagepkg1

import(
"fmt"

"github.com/spf13/viper"
)

funcFoo(){
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)
ifviper.IsSet("log.path1"){
logPath1:=viper.GetString("log.path1")
fmt.Printf("path1=[%s]\n",logPath1)
}else{
fmt.Printf("log.path1isnotfound\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。

参考资料

[1]

本文永久链接:https://tonybai.com/2021/07/10/read-ini-config-item-by-passing-section-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



推荐阅读

  • Go:神奇的init函数


福利

我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。gongzhong号 「polarisxu」,回复ebook获取;还可以回复「进群」,和数万 Gopher 交流学习。

相关资源