简介
go-ini是 Go 语言中用于操作 ini 文件的第三方库。
本文介绍
go-ini
库的使用。
ini配置文件介绍
ini 是 Windows 上常用的配置文件格式。MySQL 的 Windows 版就是使用 ini 格式存储配置的。
.ini
文件是Initialization File的缩写,即初始化文件,ini文件格式:[节/section/分区/表/段]+键=值。- 节可以为空,但参数(key=value)就需要写在开头。因为一个section没有明显的结束标识符,一个section的开始就是上一个section的结束,或者是文件结束。
- 所有的section名称都是独占一行,并且section名字都被方括号包围着([和])。
ini
文件不支持多个方括号嵌套。有的就不以ini配置文件格式读取。- ini配置文件后缀不一定是
.ini
,也可以是.cfg
、.conf
或者是.txt
。
- ini配置文件后缀不一定是
- 节名区分大小写,建议用_连接。
- 所有的参数都是以section为单位结合在一起的。可以有多个参数,但一个参数独占一行。
- 在section声明后的所有parameters都属于该section。
- 区分大小写,建议用_连接。
注释(comments)使用分号表示(;)或者#号,在分号、#号后面的文字,直到该行结尾都全部为注释。
1 2 3 4 5
# app=name app_name = awesome web [mysql] ip = 127.0.0.1 ; database=mysql
.ini
文件是windows的系统配置文件,统管windows的各项配置,最重要的就是“System.ini”、“System32.ini”和“Win.ini”。该文件主要存放用户所做的选择以及系统的各种参数。用户可以通过修改INI文件,来改变应用程序和系统的很多配置。一般用户就用windows提供的各项图形化管理界面就可实现相同的配置了,但在某些情况,还是要直接编辑.ini才方便,一般只有很熟悉windows才能去直接编辑。- 在Windows系统中,注册表的出现,让INI文件在Windows系统的地位就开始不断下滑,因为注册表独特优点,使应用程序和系统都把许多参数和初始化信息存放进了注册表中。但在某些场合,INI文件还拥有不可替代的地位。
在ini配置文件中,可以使用占位符
%(name)s
表示用之前已定义的键name
的值来替换,这里的s
表示值为字符串类型。4- 在section名称中可以用
.
来表示两个或多个分区之间的父子关系。
1 2 3 4 5 6 7 8
NAME = ini VERSION = v1 IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s [package] CLONE_URL = https://%(IMPORT_PATH)s [package.sub]
package
的没有父分区。- 如果某个键在子分区中不存在,则会在它的父分区中再次查找,直到没有父分区为止。
- 在section名称中可以用
ini文件键值如果存在多行用
"""""
包裹。一行写不下可以使用
\
,另起一行。IgnoreContinuation
可以忽略连续行。
快速使用
go-ini 是第三方库,使用前需要安装[推荐】:
|
|
也可以使用 GitHub 上的仓库:
|
|
为什么推荐
gopkg.in
,参考文章:gopkg.in介绍
首先,创建一个my.ini
配置文件:
|
|
使用 go-ini 库读取:
|
|
在 ini 文件中,每个键值对占用一行,中间使用=
隔开,可以有空格,但不是必须得。以#
开头的内容为注释。ini 文件是以分区(section)组织的。 分区以[name]
开始,在下一个分区前结束。所有分区前的内容属于默认分区,如my.ini
文件中的app_name
和log_level
。
使用go-ini
读取配置文件的步骤如下:
- 首先调用
ini.Load
加载文件,得到配置对象cfg
; - 然后以分区名调用配置对象的
Section
方法得到对应的分区对象section
,默认分区的名字为""
,也可以使用ini.DefaultSection
; - 以键名调用分区对象的
Key
方法得到对应的配置项key
对象; - 由于文件中读取出来的都是字符串,
key
对象需根据类型调用对应的方法返回具体类型的值使用,如上面的String
、MustInt
方法。
运行以下程序,得到输出:
|
|
配置文件中存储的都是字符串,所以类型为字符串的配置项不会出现类型转换失败的,故String()
方法只返回一个值。 但如果类型为Int/Uint/Float64
这些时,转换可能失败。所以Int()/Uint()/Float64()
返回一个值和一个错误。
要留意这种不一致!如果我们将配置中 redis 端口改成非法的数字 x6381,那么运行程序将报错:
|
|
Must*
便捷方法
如果每次取值都需要进行错误判断,那么代码写起来会非常繁琐。为此,go-ini
也提供对应的MustType
(Type 为Init/Uint/Float64
等)方法,这个方法只返回一个值。 同时它接受可变参数,如果类型无法转换,取参数中第一个值返回,并且该参数设置为这个配置的值,下次调用返回这个值:
|
|
配置文件还是 redis 端口为非数字 x6381 时的状态,运行程序:
|
|
我们看到第一次调用Int
返回错误,以 6381 为参数调用MustInt
之后,再次调用Int
,成功返回 6381。MustInt
源码也比较简单:
|
|
加载ini文件对象
go-ini支持从多个数据源加载ini配置文件。
数据源 可以是
[]byte
类型的原始数据,string
类型的文件路径或io.ReadCloser
。可以加载这三个 任意多个 数据,如果是其他的类型会返回错误。调用Load(source interface{}, others ...interface{})
函数。- 当创建好ini文件对象之后,我们还可以往里面添加数据源。调用
func (f *File) Append(source interface{}, others ...interface{})
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
package main import ( "bytes" "fmt" "io" "log" "strings" "github.com/go-ini/ini" ) func main() { raw := []byte(`raw=原始数据`) noClose := strings.NewReader("string=noClose") cfg, err := ini.Load(raw, "./my.cfg", io.NopCloser(noClose), io.NopCloser(bytes.NewBufferString("close=have closer")), ) if err != nil { log.Fatal("Load error:", err) } fmt.Println(cfg.Section("").Key("raw").String()) fmt.Println(cfg.Section("").Key("username").String()) fmt.Println(cfg.Section("").Key("string").String()) fmt.Println(cfg.Section("").Key("close").String()) err = cfg.Append(io.NopCloser(bytes.NewReader([]byte("append=append")))) if err != nil { log.Fatal("Append error:", err) } fmt.Println(cfg.Section("").Key("append").String()) }
io.NopCloser
函数(no optiontion closer)是将没有Close
方法的Reader
添加Close方法(转换成实现ReadCloser接口的Reader),只不过是为了防止向bytes.NewReader
、strings.NewReader
这样的Reader
没有Close
方法,底层在自动关闭的时候出错。没有关闭操作的Reader,关闭时没任何操作,有的调用自身的Close方法。运行程序输出:
1 2 3 4 5 6
$ go run main.go 原始数据 arlettebrook noClose have closer append
- 当创建好ini文件对象之后,我们还可以往里面添加数据源。调用
还可以创建一个没有任何数据源的文件对象。调用
Empty
函数。1
cfg := ini.Empty()
调用用
LooseLoad
的函数加载文件对象,若指定的文件不存在,不会返回错误。Load
会返回错误。更牛逼的是,当那些之前不存在的文件在重新调用 Reload() 方法的时候突然出现了,那么它们会被正常加载。
- 源码是:创建文件对象的时候会加载一次,创建完毕之后又会加载一次。
1
cfg, err := ini.LooseLoad("filename", "filename_404")
默认情况下,当多个数据源中有相同的键时,后面的数据源会覆盖前面的数据源。
- 调用
ShadowLoad
函数,创建的数据源不会覆盖存在的值。
- 调用
自定义加载ini文件对象
实现上调用Load
、LooseLoad
、InsensitiveLoad
(后面会介绍)、ShadowLoad
加载不同配置的文件对象,底层都是调用LoadSources(opts LoadOptions, source interface{}, others ...interface{})
函数实现的。不同的配置是通过LoadOptions
配置的。后面的参数都是数据源,默认必须有一个数据源:
|
|
为了方便使用,都将不同的配置封装到了不同的函数。
所以利用LoadSources
我们可以实现自定义加载不同配置的文件对象。
加载选项LoadOptions
常用的属性:
Loose:是否忽略文件路径不存在的错误。
Insensitive:是否启用不敏感加载,作用:忽略键名的大小写。底层是将键都转换为小写。键名包括分区名。
AllowShadows:是否不覆盖存在键的值。开启不覆盖之后,可以调用
ValueWithShadows
方法,获取指定分区下所有的重复键的值。UnescapeValueDoubleQuotes:是否强制忽略键值两端的双引号。用在多个双引号的值中。
SkipUnrecognizableLines:是否跳过无法识别的行。默认无法识别就会报错。
IgnoreContinuation:是否忽略连续换行。就是键值不支持换换行写
\
。UnparseableSections:标记一个分区为无法解析。当获取无法解析的分区时,调用Body方法会获取该分区的原始数据,未标记无法获取,同时未标记一个无法解析的分区,解析会报错。除非开跳过无法解析的行。
AllowBooleanKeys: 是否开启布尔键。开启允许只有一个键,而没有值。解析不会报错。值永远为true。保存时也只有键。
AllowPythonMultilineValues:是否允许解析多行值,用于解析换行之后对齐的字符串。
1 2 3 4 5
str = --- a b c ---
开启后类似上面的字符串都可以解析。
IgnoreInlineComment:忽略行内注释。
SpaceBeforeInlineComment:要求注释符号前必须带有一个空格
示例:
|
|
my.cfg
:
|
|
注意事项
默认情况下,本库会在您进行读写操作时采用锁机制来确保数据时间。但在某些情况下,您非常确定只进行读操作。此时,您可以通过设置 cfg.BlockMode = false
来将读操作提升大约 50-70% 的性能。
操作分区(Section)
获取分区
在加载配置之后,可以通过
Sections
方法获取所有分区对象,是切片类型的*Section
对象,SectionStrings()
方法获取所有分区名。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
package main import ( "fmt" "log" "gopkg.in/ini.v1" ) func main() { cfg, err := ini.Load("my.ini") if err != nil { log.Fatal("Fail to read file: ", err) } sections := cfg.Sections() sectionStrings := cfg.SectionStrings() for k, v := range sections { fmt.Printf("section%v: %s\n", k+1, v.Name()) } fmt.Print("sections:", sectionStrings) }
运行输出 3 个分区:
1 2 3 4 5
$ go run main.go section1: DEFAULT section2: mysql section3: redis sections:[DEFAULT mysql redis]
调用
GetSection(name)
获取指定分区,如果分区不存在,会返回错误信息。返回的分区为nil
。但调用
Section(name)
会获取指定分区,如果该分区不存在,则会自动创建指定空分区并返回:示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
package main import ( "fmt" "log" "gopkg.in/ini.v1" ) func main() { cfg, err := ini.Load("my.ini") if err != nil { log.Fatal("Fail to read file: ", err) } newSection, err := cfg.GetSection("new") if err != nil { fmt.Println(err) } fmt.Println(newSection) fmt.Println(cfg.SectionStrings()) newSection = cfg.Section("new") fmt.Println(newSection) fmt.Println(cfg.SectionStrings()) }
创建之后调用
SectionStrings
方法,新分区也会返回:1 2 3 4 5 6
$ go run main.go section "new" does not exist <nil> [DEFAULT mysql redis] &{0xc000152000 new map[] [] map[] false } [DEFAULT mysql redis new]
也可以手动创建一个新分区,如果分区已存在,则返回存在的分区:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
package main import ( "fmt" "log" "gopkg.in/ini.v1" ) func main() { cfg, err := ini.Load("my.ini") if err != nil { log.Fatal("Fail to read file: ", err) } fmt.Println(cfg.SectionStrings()) mysqlSection, err := cfg.NewSection("mysql") if err != nil { fmt.Println(err) } fmt.Println(mysqlSection.Keys()) fmt.Println(cfg.SectionStrings()) newSection, err := cfg.NewSection("new") if err != nil { fmt.Println(err) } fmt.Println(newSection.Keys()) fmt.Println(cfg.SectionStrings()) }
运行输出:
1 2 3 4 5 6
$ go run main.go [DEFAULT mysql redis] [127.0.0.1 3306 root 123456 awesome] [DEFAULT mysql redis] [] [DEFAULT mysql redis new]
读取父子分区
递归读取键值
在获取所有键值的过程中,特殊语法 %(<name>)s
会被应用,其中 <name>
可以是相同分区或者默认分区下的键名。字符串 %(<name>)s
会被相应的键值所替代,如果指定的键不存在,则会用空字符串替代(我测试是保留字符串)。您可以最多使用 99 层的递归嵌套。
在ini配置文件中,可以使用占位符%(name)s
表示用之前已定义的键name
的值来替换,这里的s
表示值为字符串类型:
|
|
上面在默认分区中设置IMPORT_PATH
的值时,使用了前面定义的NAME
和VERSION
。 在package
分区中设置CLONE_URL
的值时,使用了默认分区中定义的IMPORT_PATH
。
我们还可以在分区名中使用.
表示两个或多个分区之间的父子关系,例如package.sub
的父分区为package
,package
没有父分区。 如果某个键在子分区中不存在,则会在它的父分区中再次查找,直到没有父分区为止:
|
|
运行程序输出:
|
|
子分区中package.sub
中没有键CLONE_URL
,返回了父分区package
中的值。
package
分区中没有USERNAME
,它并没有父分区,所以返回空字符串。(调用Key
方法如果键不存在,会创建该键,值为空字符串。)后面会介绍。
操作键(Key)
获取键
- 在指定分区调用
GetKey
方法,可以获取指定的键。如果键不存在,会返回Error对象和nil。- 和分区一样,也可以直接获取键而忽略错误处理,调用
Key
方法获取指定的键,如果键不存在,会创建该键,值为空字符串。
- 和分区一样,也可以直接获取键而忽略错误处理,调用
示例如下:
|
|
运行程序输出:
|
|
默认分区中,不存在app_name123
所以GetKy返回Error和nil。而Key方法返回值为空字符串的*Key
类型。
键的其他操作
在某个分区下,调用
HasKey
方法,能判断该键是否存在。在某个分区下,调用
NewKey
方法,能够在指定分区下创建键,有两个参数,第一个:键名,第二个:值。- 这与创建分区不一样,分区如果存在,会返回存在的分区。
- 键如果存在,会覆盖值。
在某个分区下,调用
Keys
方法,能够获取指定分区下所有的*Key
对象,是[]*Key
类型。- 与
SectionStrings
方法差不多,调用KeyStrings
方法,能够获取所有的键名,是[]string
类型。
- 与
在某分区下,调用
KeysHash
方法,能够获取该分区下的所有键值对的map集合。键和值的类型都为string。示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
package main import ( "fmt" "log" "gopkg.in/ini.v1" ) func main() { cfg, err := ini.Load("my.ini") if err != nil { log.Fatal("Fail to read file: ", err) } key := cfg.Section(ini.DefaultSection).HasKey("app_name") if key { fmt.Println(cfg.Section(ini.DefaultSection).Key("app_name").String()) } newKey, err := cfg.Section(ini.DefaultSection).NewKey("app_name", "awesome go") if err != nil { log.Fatal(err) } fmt.Println(newKey.String()) fmt.Printf("%#v\n", cfg.Section("").Keys()) fmt.Println(cfg.Section("").KeyStrings()) keysHash := cfg.Section("").KeysHash() for k, v := range keysHash { fmt.Printf("%s=%s\n", k, v) } }
运行程序输出:
1 2 3 4 5 6 7
$ go run main.go awesome web awesome go []*ini.Key{(*ini.Key)(0xc00010a690), (*ini.Key)(0xc00010a700)} [app_name log_level] app_name=awesome go log_level=DEBUG
获取上级父分区下所有的键对象。
1
cfg.Section("package.sub").ParentKeys()
当键名为
-
表示自增键名,在程序中是从#1开始,#number表示,分区之间是相互独立的。1 2 3 4
[features] -: Support read/write comments of keys and sections -: Support auto-increment of key names -: Support load multiple files to overwrite key values
1
cfg.Section("features").KeyStrings() // []{"#1", "#2", "#3"}
忽略键名的大小写
默认情况下分区名和键名都区分大小写,当调用
ini.InsensitiveLoad
方法加载配置文件时,能够将所有分区和键名在读取里强制转换为小写,这样当在获取分区或者键的时候,所指定的分区名或键名不区分大小写:1 2 3 4 5 6 7 8 9 10
cfg, err := ini.InsensitiveLoad("filename") //... // sec1 和 sec2 指向同一个分区对象 sec1, err := cfg.GetSection("Section") sec2, err := cfg.GetSection("SecTIOn") // key1 和 key2 指向同一个键对象 key1, err := sec1.GetKey("Key") key2, err := sec2.GetKey("KeY")
- 为什么在加载的时候开启转换为小写,在调用的时候就能忽略大小?
- 因为在调用的时候会判断是否开启转换为小写,是会将查询的分区名或键名强制转换为小写。都转换为小写了,也就能够获取了。
- 为什么在加载的时候开启转换为小写,在调用的时候就能忽略大小?
操作键值(Value)
获取一个类型为字符串(string)的值:
|
|
获取值的同时通过自定义函数进行处理验证:
|
|
如果您不需要任何对值的自动转变功能(例如递归读取),可以直接获取原值(这种方式性能最佳):
|
|
判断某个原值是否存在:
|
|
获取其它类型的值调用对应类型的方法。返回值带有Error信息,如果不需要Error信息可以调用MustXxx方法。该方法可以指定默认值,用于转换失败的默认值。没有指定默认值为对应类型的零值。
|
|
""""""
包裹的多行字符串跟普通的获取方式一样。\
:一行写不下换行写,也是跟普通的获取方式一样,只不过属性IgnoreContinuation
,可以忽略连续换行。就是\不起作用。默认情况下字符串中只有两端有引号,无论是单、双、三,都会自动剔除。但当字符串里面有与两端相同的引号,那么引号都会保留。
UnescapeValueDoubleQuotes
属性会移除两端的双引号,只能是双引号。
获取值的时候我们还可以指定候选。如果配置文件中的值不是候选中的值,那么将选用默认值,默认值可以不是候选里面的值。string类型是
In
方法,其他的是InXxx
方法1 2
v := cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"}) v = cfg.Section("").Key("INT").InInt(10, []int{10, 20, 30})
验证获取的值是否在指定范围内:有三个参数:第一个:没有在范围内的默认值。第二个:最小值。第三个:最大值。string类型没有范围。
1 2 3 4
vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2) vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20) vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime) vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
自动分割键值到切片(slice)。作用:获取一个键的多个值。方法是对应类型加s,并指定分隔符。
- 当存在无效输入时,使用零值代替。
- 注意分隔符不能为空字符串,会出现死循环。可以为空格。
- 当在前面加上
ValidXxxs
,存在无效输入时,会忽略掉。 - 当在前面加上
StrictXxxs
,存在无效输入时,直接返回错误。
1 2 3
vals = cfg.Section("").Key("INTS").Ints(",") vals = cfg.Section("").Key("INTS").ValidInts(",") vals = cfg.Section("").Key("INTS").StrictInts(",")
修改键的值,调用
SetValue
方法。1 2
username := cfg.Section("").Key("username") username.SetValue("Mark")
在某分区下调用
NewBooleanKey
方法,会创建布尔键,值永远为true。保存时只有键名。解析时注意要开启AllowBooleanKeys,否则会报错。1
key, err := sec.NewBooleanKey("skip-host-cache")
默认情况下后面出现的键会覆盖前面存在的键,当开启
AllowShadows
配置选项时,就是调用ShadowLoad
加载数据源。后出现的键不会覆盖前面的值。还可以通过ValueWithShadows
方法获取指定分区下重复键的所有值。
操作注释(Comment)
下述几种情况的内容将被视为注释:
- 所有以
#
或;
开头的行 - 所有在
#
或;
之后的内容 - 分区标签后的文字 (即
[分区名]
之后的内容)
如果你希望使用包含 #
或 ;
的值,请使用 ``` 或 """
进行包覆。
除此之外,您还可以通过 LoadOptions 完全忽略行内注释:
|
|
或要求注释符号前必须带有一个空格:
|
|
在分区或者键上调用Comment
属性,会获取该分区或者键的所有注释(能获取头上和后边的):
|
|
运行程序输出:
|
|
保存配置
将配置保存到某个文件,调用SaveTo
或SaveToIndent
,第二个方法多一个参数,用于指定分区下键的缩进(除默认分区),可以是\t
等:
|
|
还可以写入到任何实现 io.Writer
接口的对象中,也是提供了两个方法WriteTo
、WriteToIndent
:第二个可以指定分区下键的缩进(除默认分区):
|
|
默认情况下,空格将被用于对齐键值之间的等号以美化输出结果,以下代码可以禁用该功能:
|
|
下面我们通过程序生成前面使用的配置文件my.ini
并保存:
|
|
运行程序,生成两个文件my.ini
和my-pretty.ini
,同时控制台输出文件内容。
my.ini
:
|
|
my-pretty.ini
:
|
|
*Indent
方法会对默认分区以外分区下的键增加缩进,看起来美观一点。
分区与结构体字段映射
映射到结构体
调用
MapTo
函数或者方法,可以将文件对象映射到结构体。- 当MapTo为方法时,对象是文件对象或分区,参数是要映射的结构体。
- 为了使用方便,直接将MapTo封装成了函数,该函数接收两个参数,第一个参数:结构体。第二个:数据源。
- 当对象为分区时,映射到一个分区
- 创建结构体的时候可以指定默认值,如果数据源没有或类型解析错误将使用默认值
示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
package main import ( "fmt" "log" "gopkg.in/ini.v1" ) type Config struct { AppName string `ini:"app_name"` LogLevel string `ini:"log_level"` MySQL MySQLConfig `ini:"mysql"` Redis RedisConfig `ini:"redis"` } type MySQLConfig struct { IP string `ini:"ip"` Port int `ini:"port"` User string `ini:"user"` Password string `ini:"password"` Database string `ini:"database"` } type RedisConfig struct { IP string `ini:"ip"` Port int `ini:"port"` } func main() { cfg, err := ini.Load("my.ini") if err != nil { log.Fatal("load my.ini failed: ", err) } assertMapToError := func(e error) { if e != nil { log.Fatal("MapTo error:", err) } } c1 := new(Config) err = cfg.MapTo(&c1) assertMapToError(err) fmt.Println(c1) c2 := new(Config) err = ini.MapTo(c2, "my.ini") assertMapToError(err) fmt.Println(c2) m := &MySQLConfig{ IP: "localhost", } err = cfg.Section("mysql").MapTo(m) assertMapToError(err) fmt.Println(m) }
MapTo
内部使用了反射,所以结构体字段必须都是导出的。如果键名与字段名不相同,那么需要在结构标签中指定对应的键名。 这一点与 Go 标准库encoding/json
和encoding/xml
不同。标准库json/xml
解析时可以将键名app_name
对应到字段名AppName
。而go-ini
需要[自定义键名映射器](#键名映射器(Name Mapper))才能实现这种效果。运行程序输出:
1 2 3 4
$ go run main.go &{awesome web DEBUG {127.0.0.1 3306 root 123456 awesome} {127.0.0.1 6381}} &{awesome web DEBUG {127.0.0.1 3306 root 123456 awesome} {127.0.0.1 6381}} &{127.0.0.1 3306 root 123456 awesome}
从结构体反射
我们可以调用
ReflectFrom
函数或方法,将结构体反射成文件对象。当为方法时,对象是反射到的文件对象或分区,参数是结构体。
- 为了使用方便,将其封装成了函数,接收两个参数。第一个:反射到的文件对象,第二个:结构体
当对象为分区时,反射到分区。
注意当结构体字段与配置键不同名时需要用结构体标签指定。
支持的标签:
ini:指定键名,或者分区名。
有第二个参数omitempty,用
,
分隔开。值为空时,省略掉,不写入文件对象。有第三参数allowshadow,如果不需要前两个标签规则,可以使用
ini:",,allowshadow"
进行简写。- 作用:将一个键的不同值分行保存,不用分隔符分开。
1 2 3 4
[IP] value = 192.168.31.201 value = 192.168.31.211 value = 192.168.31.221
1 2 3
type IP struct { Value []string `ini:"value,omitempty,allowshadow"` }
comment:指定注释,保存到配置注释会在键的头上。
delim:指定分隔符。一个键存在多个值的情况,需要指定分隔符。
1 2 3 4 5 6 7 8 9 10 11 12 13
type Embeded struct { Dates []time.Time `delim:"|" comment:"Time data"` Places []string `ini:"places,omitempty"` None []int `ini:",omitempty"` } ... Embeded{ []time.Time{time.Now(), time.Now()}, []string{"HangZhou", "Boston"}, []int{}, } ...
1 2 3 4 5
; Embeded section [Embeded] ; Time data Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00 places = HangZhou,Boston
示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
package main import ( "fmt" "log" "gopkg.in/ini.v1" ) type Config struct { AppName string `ini:"app_name"` LogLevel string `ini:"log_level"` MySQL MySQLConfig `ini:"mysql"` Redis RedisConfig `ini:"redis"` } type MySQLConfig struct { IP string `ini:"ip"` Port int `ini:"port"` User string `ini:"user"` Password string `ini:"password"` Database string `ini:"database"` } type RedisConfig struct { IP string `ini:"ip"` Port int `ini:"port"` } func main() { cfg1 := ini.Empty() c1 := Config{ AppName: "awesome web", LogLevel: "DEBUG", MySQL: MySQLConfig{ IP: "127.0.0.1", Port: 3306, User: "root", Password: "123456", Database: "awesome", }, Redis: RedisConfig{ IP: "127.0.0.1", Port: 6381, }, } assertReflectError := func(e error) { if e != nil { log.Fatal("Reflect error:", e) } } err := ini.ReflectFrom(cfg1, &c1) assertReflectError(err) fmt.Println(cfg1.Section("").Key("app_name").String()) c2 := Config{ AppName: "awesome go", } cfg2 := ini.Empty() err = cfg2.ReflectFrom(&c2) assertReflectError(err) fmt.Println(cfg2.Section("").Key("app_name").String()) m := MySQLConfig{ IP: "localhost", } cfg3 := ini.Empty() err = cfg3.Section("mysql").ReflectFrom(&m) assertReflectError(err) fmt.Println(cfg3.Section("mysql").Key("ip").String()) }
运行程序输出:
1 2 3 4
$ go run main.go awesome web awesome go localhost
映射/反射的其它说明
任何嵌入的结构都会被默认认作一个不同的分区,并且不会自动产生所谓的父子分区关联:
|
|
示例配置文件:
|
|
如果需要指定嵌入结构体是同一个分区,需要指定标签指定分区名如:ini:“Parent”。示例如下:
|
|
示例配置文件:
|
|
自定义键名和键值映射器
键名映射器(Name Mapper)
当我们利用结构体标签指定键名时,会觉得太麻烦。为了节省时间并简化代码,go-ini
库支持类型为 NameMapper
的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。
目前有 2 款内置的映射器:
AllCapsUnderscore
:该映射器将字段名转换至格式ALL_CAPS_UNDERSCORE
后再去匹配分区名和键名。TitleUnderscore
:该映射器将字段名转换至格式title_underscore
后再去匹配分区名和键名。
使用方法:只需要将映射MapTo
、反射ReflectFrom
函数后面加上WithMapper
,传惨时,传入对应映射器即可。或者给指定的文件对象指定映射器。属性是 NameMapper
,示例如下:
|
|
键值映射器(Value Mapper)
值映射器允许使用一个自定义函数自动展开值的具体内容,例如在运行时获取环境变量:
|
|
运行程序输出:
|
|
会输出你电脑的用户名。
总结
本文简单介绍了ini配置文件格式,内容来自互联网,仅供参考。还介绍了go-ini
库,基本上参考的是其官方文档,官方文档写的非常详细,推荐去看,而且有中文。 作者是无闻,相信做 Go 开发的都不陌生。
参考
- ini配置文件格式
- go-ini GitHub 仓库
- go-ini 官方文档
- Go 每日一库之 go-ini