Go 配置文件管理(ini)

麦浪11个月前技术文章403

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、结果检查

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




相关文章

MySQL DDL 风险评估

MySQL DDL 风险评估

一、前言变更是数据库离不开的话题,从 MySQL 5.6 开始,推出 online DDL 即变更期间不锁表,本篇文章介绍 MySQL 变更对数据库的影响如何去判断。二、DDL 风险提示1. 变更速查...

Kafka 单条日志传输大小

kafka 对于消息体的大小默认为单条最大值是1M 但是在我们应用场景中, 常常会出现一条消息大于1M,如果不对kafka 进行配置。则会出现生产者无法将消息推送到kafka 或消费者无法去消费kaf...

MySQL 小数类型介绍

MySQL 小数类型介绍

前言对于保证精度的数字,MySQL 也有对应的小数类型,下图是 MySQL 中小数类型概览。 浮点:小数点非固定的数,可表示数据范围较广,整数,小数都可表示。定点:小数点固定,可表示整数,小数。int...

MySQL运维实战(3.1) MySQL官方客户端使用介绍

mysql是mysql安装包默认的客户端。位于二进制安装包的bin目录。或者通过rpm安装包安装mysql-community-client。使用mysql程序linux终端下,输入mysql命令登陆...

pg_dump

逻辑备份    PG提供了pg_dump、pg_dumpall两种方式进行逻辑备份,其区别就是pg_dumpall只能将数据库全部数据集dump到一个脚本文件中,而pg_dump可以选择指定数据库进行...

Scheduler调度器

一、论 Pod 调度在 kubernetes 中,无论是 Deployment、Statefulset 等多种控制器,它最终都是创建 Pod,在 Pod 创建是需要被调度到 Kubernetes 集群...

发表评论    

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