Go 配置文件管理(ini)
1、背景
ini 文件是 Initialization File 的缩写,即初始化文件,可用于统一管理各项配置。
gopkg.in/ini.v1 是地表最强大、最方便和最流行的 Go 语言 INI 文件操作库。支持灵活的数据源、多种格式兼容、自然类型增强、结构体映射、超神般的辅助及高度自定义。
2、操作前了解相关配置和要求
了解 ini 文件格式规范
了解 go 语言基本语法
3、操作步骤
3.1 安装
前提:
最低要求安装 Go 语言版本为 1.13。
安装 :
$ go get gopkg.in/ini.v1
更新 :
$ go get -u gopkg.in/ini.v1
3.2 加载数据源
一个数据源可以是 []byte
类型的原始数据,string
类型的文件路径或 io.ReadCloser
。我们可以加载任意多个数据源。如果您传递其它类型的数据源,则会直接返回错误。
cfg, err := ini.Load( []byte("raw data"), // 原始数据 "filename", // 文件路径 ioutil.NopCloser(bytes.NewReader([]byte("some other data"))), )
或者从一个空白的文件开始。
cfg := ini.Empty()
在一开始无法决定需要加载哪些数据源时,仍可以使用 Append() 在需要的时候加载它们。
err := cfg.Append("other file", []byte("other raw data"))
当您想要加载一系列文件,但是不能够确定其中哪些文件是不存在的,可以通过调用函数 LooseLoad() 来忽略它们。
cfg, err := ini.LooseLoad("filename", "filename_404")
3.2.1 跳过无法识别的行
某些情况下,配置文件可能包含非键值对的数据行,解析器默认会报错并终止解析。如果希望解析器能够忽略并它们完成剩余内容的解析,则可以通过如下方法实现:
cfg, err := ini.LoadSources(ini.LoadOptions{ SkipUnrecognizableLines: true, }, "other.ini")
3.2.2 保存配置
终于到了这个时刻,是时候保存一下配置了。
比较原始的做法是输出配置到某个文件:
// ... err = cfg.SaveTo("my.ini") err = cfg.SaveToIndent("my.ini", "\t")
另一个比较高级的做法是写入到任何实现 io.Writer
接口的对象中:
// ... cfg.WriteTo(writer) cfg.WriteToIndent(writer, "\t")
默认情况下,空格将被用于对齐键值之间的等号以美化输出结果,以下代码可以禁用该功能:
ini.PrettyFormat = false
3.3 操作 Session
[DEFAULT] user=mysql [mysql] default-character-set=utf8 [mysqld] datadir =/dbserver/data port = 33060 character-set-server=utf8
3.3.1 获取默认分区
方式一:使用空字符串代替分区名,来获取默认分区
package main import ( "fmt" "gopkg.in/ini.v1" ) func main() { cfg, _ := ini.Load("config.ini") sec, _ := cfg.GetSection("") // 获取默认分区 fmt.Println(sec.KeyStrings()) // [user] }
方式二:使用 ini.DEFAULT_SECTION
作为分区名,来获取默认分区
package main import ( "fmt" "gopkg.in/ini.v1" ) func main() { cfg, _ := ini.Load("config.ini") sec, _ := cfg.GetSection(ini.DEFAULT_SECTION) // 获取默认分区 fmt.Println(sec.KeyStrings()) // [user] }
3.3.2 获取指定分区
package main import ( "fmt" "gopkg.in/ini.v1" ) func main() { cfg, _ := ini.Load("config.ini") sec, _ := cfg.GetSection("mysqld") // 获取指定分区 fmt.Println(sec.KeyStrings()) // [datadir port character-set-server] }
当我们非常确定某个分区是存在的,可以使用以下简便方法:
package main import ( "fmt" "gopkg.in/ini.v1" ) func main() { cfg, _ := ini.Load("config.ini") sec := cfg.Section("mysqld") // 获取指定分区 fmt.Println(sec.KeyStrings()) // [datadir port character-set-server] }
3.3.3 获取所有分区
我们可以获取所有分区对象或名称。
package main import ( "fmt" "gopkg.in/ini.v1" ) func main() { cfg, _ := ini.Load("config.ini") secs := cfg.Sections() // 获取所有分区对象 names := cfg.SectionStrings() // 获取所有分区对象名称 fmt.Println("sections:", secs) // sections: [0x1400012e620 0x1400012e700 0x1400012e7e0] fmt.Println("secnames:", names) // secnames: [DEFAULT mysql mysqld] }
3.3.4 读取父子分区
我们可以在分区名称中使用 . 来表示两个或多个分区之间的父子关系。如果某个键在子分区中不存在,则会去它的父分区中再次寻找,直到没有父分区为止。
实例:
[package] nginx = nginx-1.21.1 [package.apps] go = go-1.19
package main import ( "fmt" "gopkg.in/ini.v1" ) func main() { cfg, _ := ini.Load("./config/config.ini") value := cfg.Section("package.apps").Key("nginx").String() fmt.Println(value) // nginx-1.21.1 }
3.4 操作 Key
获取某个分区下的键对应的值:
package main import ( "fmt" "gopkg.in/ini.v1" ) func main() { cfg, _ := ini.Load("config.ini") key, _ := cfg.Section("mysql").GetKey("default-character-set") // 获取mysql分区下default-character-set键对应的值 fmt.Println(key) // utf8 }
和分区一样,也可以直接获取键而忽略错误处理:
package main import ( "fmt" "gopkg.in/ini.v1" ) func main() { cfg, _ := ini.Load("config.ini") key, _ := cfg.Section("mysql").Key("default-character-set") // 获取mysql分区下default-character-set键对应的值 fmt.Println(key) // utf8 }
判断某个键是否存在:
package main import ( "fmt" "gopkg.in/ini.v1" ) func main() { cfg, _ := ini.Load("config.ini") yes1 := cfg.Section("mysql").HasKey("default-character-set") yes2 := cfg.Section("mysql").HasKey("default-character-get") fmt.Println(yes1, yes2) // true false }
创建一个新的键:
err := cfg.Section("").NewKey("name", "value")
获取分区下的所有键或键名:
keys := cfg.Section("").Keys() names := cfg.Section("").KeyStrings()
获取分区下的所有键值对的克隆:
hash := cfg.Section("").KeysHash()
3.4.1 忽略键名大小写
可以通过 InsensitiveLoad 将所有分区和键名在读取里强制转换为小写,进而起到忽略大小写的作用。
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")
3.4.2 处理无值的键名
类似 MySQL 的配置文件中会出现没有具体值的布尔类型的键:
[mysqld] ... skip-host-cache skip-name-resolve
默认情况下这被认为是缺失值而无法完成解析,但可以通过高级的加载选项对它们进行处理:
cfg, err := ini.LoadSources(ini.LoadOptions{ AllowBooleanKeys: true, }, "my.cnf")
这些键的值永远为 true
,且在保存到文件时也只会输出键名。
如果您想要通过程序来生成此类键,则可以使用 NewBooleanKey
:
key, err := sec.NewBooleanKey("skip-host-cache")
3.5 操作 Value
获取一个类型为字符串(string)的值:
val := cfg.Section("").Key("key name").String()
获取值的同时通过自定义函数进行处理验证:
val := cfg.Section("").Key("key name").Validate(func(in string) string { if len(in) == 0 { return "default" } return in })
如果您不需要任何对值的自动转变功能(例如递归读取),可以直接获取原值(这种方式性能最佳):
val := cfg.Section("").Key("key name").Value()
判断某个原值是否存在:
yes := cfg.Section("").HasValue("test value")
获取其它类型的值:
// 布尔值的规则: // true 当值为:1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On // false 当值为:0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off v, err = cfg.Section("").Key("BOOL").Bool() v, err = cfg.Section("").Key("FLOAT64").Float64() v, err = cfg.Section("").Key("INT").Int() v, err = cfg.Section("").Key("INT64").Int64() v, err = cfg.Section("").Key("UINT").Uint() v, err = cfg.Section("").Key("UINT64").Uint64() v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339) v, err = cfg.Section("").Key("TIME").Time() // RFC3339 v = cfg.Section("").Key("BOOL").MustBool() v = cfg.Section("").Key("FLOAT64").MustFloat64() v = cfg.Section("").Key("INT").MustInt() v = cfg.Section("").Key("INT64").MustInt64() v = cfg.Section("").Key("UINT").MustUint() v = cfg.Section("").Key("UINT64").MustUint64() v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339) v = cfg.Section("").Key("TIME").MustTime() // RFC3339 // 由 Must 开头的方法名允许接收一个相同类型的参数来作为默认值, // 当键不存在或者转换失败时,则会直接返回该默认值。 // 但是,MustString 方法必须传递一个默认值。 v = cfg.Section("").Key("String").MustString("default") v = cfg.Section("").Key("BOOL").MustBool(true) v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25) v = cfg.Section("").Key("INT").MustInt(10) v = cfg.Section("").Key("INT64").MustInt64(99) v = cfg.Section("").Key("UINT").MustUint(3) v = cfg.Section("").Key("UINT64").MustUint64(6) v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now()) v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339
获取键值时设定候选值:
v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"}) v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75}) v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30}) v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30}) v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9}) v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9}) v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3}) v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339
如果获取到的值不是候选值的任意一个,则会返回默认值,而默认值不需要是候选值中的一员。
验证获取的值是否在指定范围内:
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("INT64").RangeInt64(0, 10, 20) vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9) vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9) vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime) vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
递归读取键值:
在获取所有键值的过程中,特殊语法 %(<name>)s
会被应用,其中 <name>
可以是相同分区或者默认分区下的键名。字符串 %(<name>)s
会被相应的键值所替代,如果指定的键不存在,则会用空字符串替代。您可以最多使用 99 层的递归嵌套。
NAME = ini [author] NAME = Unknwon GITHUB = https://github.com/%(NAME)s [package] FULL_NAME = github.com/go-ini/%(NAME)s
cfg.Section("author").Key("GITHUB").String() // https://github.com/Unknwon cfg.Section("package").Key("FULL_NAME").String() // github.com/go-ini/ini
4、注意事项
注意使用错误处理来提高程序的容错性。
5、结果检查
对程序进行测试以便验证结果是否正常。