INI 文件是 Initialization File 的缩写,即初始化文件,是 Windows 的系统配置文件所采用的存储格式,统管 Windows 的各项配置。INI 文件格式由节(section)和键(key)构成,一般用于操作系统、虚幻游戏引擎、GIT 版本管理中,这种配置文件的文件扩展名为.ini。
下面是从 GIT 版本管理的配置文件中截取的一部分内容,展示 INI 文件的样式。
INI 文件由多行文本组成,整个配置由[ ]拆分为多个“段”(section)。每个段中又以=分割为“键”和“值”。
INI 文件以;置于行首视为注释,注释后将不会被处理和识别,如下所示:
熟悉了 INI 文件的格式后,下面我们创建一个 example.ini 文件,并将从 GIT 版本管理配置文件中截取的一部分内容复制到该文件中。
准备好 example.ini 文件后,下面我们开始尝试读取该 INI 文件,并从文件中获取需要的数据,完整的示例代码如下所示:
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
// 根据文件名,段名,键名获取ini的值
func getValue(filename, expectSection, expectKey string) string {
// 打开文件
file, err := os.Open(filename)
// 文件找不到,返回空
if err != nil {
return ""
}
// 在函数结束时,关闭文件
defer file.Close()
// 使用读取器读取文件
reader := bufio.NewReader(file)
// 当前读取的段的名字
var sectionName string
for {
// 读取文件的一行
linestr, err := reader.ReadString('\n')
if err != nil {
break
}
// 切掉行的左右两边的空白字符
linestr = strings.TrimSpace(linestr)
// 忽略空行
if linestr == "" {
continue
}
// 忽略注释
if linestr[0] == ';' {
continue
}
// 行首和尾巴分别是方括号的,说明是段标记的起止符
if linestr[0] == '[' && linestr[len(linestr)-1] == ']' {
// 将段名取出
sectionName = linestr[1 : len(linestr)-1]
// 这个段是希望读取的
} else if sectionName == expectSection {
// 切开等号分割的键值对
pair := strings.Split(linestr, "=")
// 保证切开只有1个等号分割的简直情况
if len(pair) == 2 {
// 去掉键的多余空白字符
key := strings.TrimSpace(pair[0])
// 是期望的键
if key == expectKey {
// 返回去掉空白字符的值
return strings.TrimSpace(pair[1])
}
}
}
}
return ""
}
func main() {
fmt.Println(getValue("example.ini", "remote \"origin\"", "fetch"))
fmt.Println(getValue("example.ini", "core", "hideDotFiles"))
}
本例并不是将整个 INI 文件读取保存后再获取需要的字段数据并返回,这里使用 getValue() 函数,每次从指定文件中找到需要的段(Section)及键(Key)对应的值。
getValue() 函数的声明如下:
参数说明如下。
getValue() 函数的实际使用例子参考代码如下:
运行上面的示例程序,输出结果如下:
输出内容中“+refs/heads/*:refs/remotes/origin/*”表示 INI 文件中[remote "origin"]的 "fetch" 键对应的值;dotGitOnly 表示 INI 文件中[core]中键名为 "hideDotFiles" 的值。
注意 main 函数的第 2 行中,由于段名中包含双引号,所以使用\进行转义。
getValue() 函数的逻辑由 4 部分组成:即读取文件、读取行文本、读取段和读取键值组成。接下来分步骤了解 getValue() 函数的详细处理过程。
Go语言的 OS 包中提供了文件打开函数 os.Open(),文件读取完成后需要及时关闭,否则文件会发生占用,系统无法释放缓冲资源。参考下面代码:
// 打开文件
file, err := os.Open(filename)
// 文件找不到,返回空
if err != nil {
return ""
}
// 在函数结束时,关闭文件
defer file.Close()
代码说明如下:
INI 文件已经打开了,接下来就可以开始读取 INI 的数据了。
INI 文件的格式是由多行文本组成,因此需要构造一个循环,不断地读取 INI 文件的所有行。Go语言总是将文件以二进制格式打开,通过不同的读取方式对二进制文件进行操作。Go语言对二进制读取有专门的代码,bufio 包即可以方便地以比较常见的方式读取二进制文件。
// 使用读取器读取文件
reader := bufio.NewReader(file)
// 当前读取的段的名字
var sectionName string
for {
// 读取文件的一行
linestr, err := reader.ReadString('\n')
if err != nil {
break
}
// 切掉行的左右两边的空白字符
linestr = strings.TrimSpace(linestr)
// 忽略空行
if linestr == "" {
continue
}
// 忽略注释
if linestr[0] == ';' {
continue
}
//读取段和键值的代码
//...
}
代码说明如下:
读取 INI 文本文件时,需要注意各种异常情况。文本中的空白符就是经常容易忽略的部分,空白符在调试时完全不可见,需要打印出字符的 ASCII 码才能辨别。
抛开各种异常情况拿到了每行的行文本 linestr 后,就可以方便地读取 INI 文件的段和键值了。
行字符串 linestr 已经去除了空白字符串,段的起止符又以[开头,以]结尾,因此可以直接判断行首和行尾的字符串匹配段的起止符匹配时读取的是段,如下图所示。
此时,段只是一个标识,而无任何内容,因此需要将段的名字取出保存在 sectionName(己在之前的代码中定义)中,待读取段后面的键值对时使用。
// 行首和尾巴分别是方括号的,说明是段标记的起止符
if linestr[0] == '[' && linestr[len(linestr)-1] == ']' {
// 将段名取出
sectionName = linestr[1 : len(linestr)-1]
// 这个段是希望读取的
}
代码说明如下:
这里代码紧接着前面的代码。当前行不是段时(不以[开头),那么行内容一定是键值对。别忘记此时 getValue() 的参数对段有匹配要求。找到能匹配段的键值对后,开始对键值对进行解析,参考下面的代码:
else if sectionName == expectSection {
// 切开等号分割的键值对
pair := strings.Split(linestr, "=")
// 保证切开只有1个等号分割的简直情况
if len(pair) == 2 {
// 去掉键的多余空白字符
key := strings.TrimSpace(pair[0])
// 是期望的键
if key == expectKey {
// 返回去掉空白字符的值
return strings.TrimSpace(pair[1])
}
}
}
代码说明如下: