Go 配置文件管理(ini)

麦浪1年前技术文章602

1、背景

ini 文件是 Initialization File 的缩写,即初始化文件,可用于统一管理各项配置。

gopkg.in/ini.v1 是地表最强大、最方便和最流行的 Go 语言 INI 文件操作库。支持灵活的数据源、多种格式兼容、自然类型增强、结构体映射、超神般的辅助及高度自定义。

2、操作前了解相关配置和要求

  1. 了解 ini 文件格式规范

  2. 了解 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、结果检查

对程序进行测试以便验证结果是否正常。




相关文章

Python 识别 MySQL 中的冗余索引

前言最近在搞标准化巡检平台,通过 MySQL 的元数据分析一些潜在的问题。冗余索引也是一个非常重要的巡检目,表中索引过多,会导致表空间占用较大,索引的数量与表的写入速度与索引数成线性关系(微秒级),如...

Serverless 技术选型

Serverless 技术选型

在 Serverless 这个大领域中,不只有函数计算这一种产品形态和应用类型,而是面向不同的用户群体和使用习惯,都有其各自适用的 Serverless 产品。例如面向函数的函数计算、面向应用的 Se...

Ranger部署

安装前准备1.1. 创建用户和用户组groupadd rangeruseradd -g ranger ranger1.2. 数据库配置mysql -uroot -p -hxxx.xxx.xxx.13 ...

 大数据集群监控配置操作指导(三)Flink监控开启jmx

大数据集群监控配置操作指导(三)Flink监控开启jmx

官网的关于 flnk+prometheus的文章https://flink.apache.org/features/2019/03/11/prometheus-monitoring.htmlprome...

AD域主备部署

AD域主备部署

总览在本篇文章中, 我将记录部署多 DC 实现高可用方案的详细步骤, 期间我会尽量使用 PowerShell 来实现相应的动作, 实在找不到命令或者 GUI 更方便的再附截图. 主要步骤分为:部署 2...

arm环境安装达梦数据库

arm环境安装达梦数据库

一、安装前准备1、创建用户和用户组groupadd dinstall useradd -g dinstall -m -d /home/dm...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。