简介
- 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