Go 配置文件管理(ini)

麦浪2年前技术文章722

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

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




相关文章

spark与mr的异同

1.MR:抽象层次低,需要使用手工代码来完成程序编写,使用上难以上手;Spark:Spark 采用RDD 计算模型,简单容易上手。2.MR:只提供map 和reduce 两个操作,表达能力欠缺;Spa...

Doris资源管理

资源管理为了节省Doris集群内的计算、存储资源,Doris需要引入一些其他外部资源来完成相关的工作,如Spark/GPU用于查询,HDFS/S3用于外部存储,Spark/MapReduce用于ETL...

数据湖技术之iceberg(六)Iceberg表数据组织与查询

数据湖技术之iceberg(六)Iceberg表数据组织与查询

1     Iceberg表数据组织与查询1) 下载avro-tools jar包由于后期需要查看avro文件内容,我们可以通过avro-tool.jar来查看...

PG的统计信息(一)

一、统计信息1.1 PG统计信息概述pg的统计信息主要分为两种:第一类统计信息是是负载指标“统计信息”(Monitoring stats),通过stat collector进程进行实时采集更新的负载指...

greenplum安装配置

一、配置服务器安装环境1、修改服务器内核参数(所有节点)编辑sysctl.conf,需要设置共享内存Greenplum数据库使用共享内存在属于同一postgres实例的postgres进程之间进行通信...

mysqldump导入备份文件报错记录

mysqldump导入备份文件报错记录

mysqldump导入备份文件报错记录【问题记录1】目前遇到过两类报错,均主要是因为 GTID 引起,两类报错内容完全不同,但解决方法相同,报错内容具体如下:第一类报错:在自建数据库往云上数据库导入数...

发表评论    

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