返回

Go-ini Introduction

Go第三方库之读写ini文件库go-ini介绍。


简介

  • go-ini是 Go 语言中用于操作 ini 文件的第三方库。

  • 本文介绍go-ini库的使用。

ini配置文件介绍

  • ini 是 Windows 上常用的配置文件格式。MySQL 的 Windows 版就是使用 ini 格式存储配置的。

  • .ini文件是Initialization File的缩写,即初始化文件,ini文件格式:[节/section/分区/表/段]+键=值。

    • 节可以为空,但参数(key=value)就需要写在开头。因为一个section没有明显的结束标识符,一个section的开始就是上一个section的结束,或者是文件结束。
    • 所有的section名称都是独占一行,并且section名字都被方括号包围着([和])。
      • ini文件不支持多个方括号嵌套。有的就不以ini配置文件格式读取。
        • ini配置文件后缀不一定是.ini,也可以是.cfg.conf或者是.txt
      • 节名区分大小写,建议用_连接。
    • 所有的参数都是以section为单位结合在一起的。可以有多个参数,但一个参数独占一行。
      • 在section声明后的所有parameters都属于该section。
      • 区分大小写,建议用_连接。
  • 注释(comments)使用分号表示(;)或者#号,在分号、#号后面的文字,直到该行结尾都全部为注释。

    1
    2
    3
    4
    5
    
    # app=name
    app_name = awesome web
    
    [mysql] 
    ip = 127.0.0.1  ; database=mysql
    
  • .ini文件是windows的系统配置文件,统管windows的各项配置,最重要的就是“System.ini”、“System32.ini”和“Win.ini”。该文件主要存放用户所做的选择以及系统的各种参数。用户可以通过修改INI文件,来改变应用程序和系统的很多配置。一般用户就用windows提供的各项图形化管理界面就可实现相同的配置了,但在某些情况,还是要直接编辑.ini才方便,一般只有很熟悉windows才能去直接编辑。

    • 在Windows系统中,注册表的出现,让INI文件在Windows系统的地位就开始不断下滑,因为注册表独特优点,使应用程序和系统都把许多参数和初始化信息存放进了注册表中。但在某些场合,INI文件还拥有不可替代的地位。
  • 在ini配置文件中,可以使用占位符%(name)s表示用之前已定义的键name的值来替换,这里的s表示值为字符串类型。4

    • 在section名称中可以用.来表示两个或多个分区之间的父子关系
    1
    2
    3
    4
    5
    6
    7
    8
    
    NAME = ini
    VERSION = v1
    IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
    
    [package]
    CLONE_URL = https://%(IMPORT_PATH)s
    
    [package.sub]
    

    package的没有父分区。

    • 如果某个键在子分区中不存在,则会在它的父分区中再次查找,直到没有父分区为止
  • ini文件键值如果存在多行用"""""包裹。

  • 一行写不下可以使用\,另起一行。

    • IgnoreContinuation可以忽略连续行。

快速使用

go-ini 是第三方库,使用前需要安装[推荐】:

1
$ go get -u gopkg.in/ini.v1

也可以使用 GitHub 上的仓库:

1
$ go get -u github.com/go-ini/ini

为什么推荐gopkg.in,参考文章:gopkg.in介绍

首先,创建一个my.ini配置文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
app_name = awesome web ; 这是注释

# possible values: DEBUG, INFO, WARNING, ERROR, FATAL
log_level = DEBUG # 这也是注释

; database=mysql
[mysql]
ip = 127.0.0.1
port = 3306
user = root
password = 123456
database = awesome

[redis]
ip = 127.0.0.1
port = 6381

使用 go-ini 库读取:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
    "fmt"
    "log"

    "gopkg.in/ini.v1"
)

func main() {
    cfg, err := ini.Load("my.ini")
    if err != nil {
       log.Fatal("Fail to read file: ", err)
    }

    fmt.Println("App Name:", cfg.Section("").Key("app_name").String())
    fmt.Println("Log Level:", cfg.Section("").Key("log_level").String())

    fmt.Println("MySQL IP:", cfg.Section("mysql").Key("ip").String())
    mysqlPort, err := cfg.Section("mysql").Key("port").Int()
    if err != nil {
       log.Fatal(err)
    }
    fmt.Println("MySQL Port:", mysqlPort)
    fmt.Println("MySQL User:", cfg.Section("mysql").Key("user").String())
    fmt.Println("MySQL Password:", cfg.Section("mysql").Key("password").String())
    fmt.Println("MySQL Database:", cfg.Section("mysql").Key("database").String())

    fmt.Println("Redis IP:", cfg.Section("redis").Key("ip").String())
    redisPort, err := cfg.Section("redis").Key("port").Int()
    if err != nil {
       log.Fatal(err)
    }
    fmt.Println("Redis Port:", redisPort)
}

在 ini 文件中,每个键值对占用一行,中间使用=隔开,可以有空格,但不是必须得。以#开头的内容为注释。ini 文件是以分区(section)组织的。 分区以[name]开始,在下一个分区前结束。所有分区前的内容属于默认分区,如my.ini文件中的app_namelog_level

使用go-ini读取配置文件的步骤如下:

  • 首先调用ini.Load加载文件,得到配置对象cfg
  • 然后以分区名调用配置对象的Section方法得到对应的分区对象section,默认分区的名字为"",也可以使用ini.DefaultSection
  • 以键名调用分区对象的Key方法得到对应的配置项key对象;
  • 由于文件中读取出来的都是字符串,key对象需根据类型调用对应的方法返回具体类型的值使用,如上面的StringMustInt方法。

运行以下程序,得到输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ go run main.go 
App Name: awesome web
Log Level: DEBUG
MySQL IP: 127.0.0.1
MySQL Port: 3306
MySQL User: root
MySQL Password: 123456
MySQL Database: awesome
Redis IP: 127.0.0.1
Redis Port: 6381

配置文件中存储的都是字符串,所以类型为字符串的配置项不会出现类型转换失败的,故String()方法只返回一个值。 但如果类型为Int/Uint/Float64这些时,转换可能失败。所以Int()/Uint()/Float64()返回一个值和一个错误。

要留意这种不一致!如果我们将配置中 redis 端口改成非法的数字 x6381,那么运行程序将报错:

1
2
2024/05/16 11:45:00 strconv.ParseInt: parsing "x6381": invalid syntax
exit status 1

Must*便捷方法

如果每次取值都需要进行错误判断,那么代码写起来会非常繁琐。为此,go-ini也提供对应的MustType(Type 为Init/Uint/Float64等)方法,这个方法只返回一个值。 同时它接受可变参数,如果类型无法转换,取参数中第一个值返回,并且该参数设置为这个配置的值,下次调用返回这个值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
    "fmt"
    "log"

    "gopkg.in/ini.v1"
)

func main() {
    cfg, err := ini.Load("my.ini")
    if err != nil {
       log.Fatal("Fail to read file: ", err)
    }

    redisPort, err := cfg.Section("redis").Key("port").Int()
    if err != nil {
       fmt.Println("before must, get redis port error:", err)
    } else {
       fmt.Println("before must, get redis port:", redisPort)
    }

    fmt.Println("redis Port:", cfg.Section("redis").Key("port").MustInt(6381))

    redisPort, err = cfg.Section("redis").Key("port").Int()
    if err != nil {
       fmt.Println("after must, get redis port error:", err)
    } else {
       fmt.Println("after must, get redis port:", redisPort)
    }
}

配置文件还是 redis 端口为非数字 x6381 时的状态,运行程序:

1
2
3
4
$ go run main.go 
before must, get redis port error: strconv.ParseInt: parsing "x6381": invalid syntax
redis Port: 6381
after must, get redis port: 6381

我们看到第一次调用Int返回错误,以 6381 为参数调用MustInt之后,再次调用Int,成功返回 6381。MustInt源码也比较简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func (k *Key) MustInt(defaultVal ...int) int {
    val, err := k.Int()
    if len(defaultVal) > 0 && err != nil {
       k.value = strconv.FormatInt(int64(defaultVal[0]), 10)
       return defaultVal[0]
    }
    return val
}

func (k *Key) Int() (int, error) {
	v, err := strconv.ParseInt(k.String(), 0, 64)
	return int(v), err
}

加载ini文件对象

  • go-ini支持从多个数据源加载ini配置文件。

  • 数据源 可以是 []byte 类型的原始数据,string 类型的文件路径io.ReadCloser。可以加载这三个 任意多个 数据,如果是其他的类型会返回错误。调用Load(source interface{}, others ...interface{})函数。

    • 当创建好ini文件对象之后,我们还可以往里面添加数据源。调用func (f *File) Append(source interface{}, others ...interface{})方法。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
    package main
    
    import (
    	"bytes"
    	"fmt"
    	"io"
    	"log"
    	"strings"
    
    	"github.com/go-ini/ini"
    )
    
    func main() {
    	raw := []byte(`raw=原始数据`)
    	noClose := strings.NewReader("string=noClose")
    
    	cfg, err := ini.Load(raw,
    		"./my.cfg",
    		io.NopCloser(noClose),
    		io.NopCloser(bytes.NewBufferString("close=have closer")),
    	)
    
    	if err != nil {
    		log.Fatal("Load error:", err)
    	}
    
    	fmt.Println(cfg.Section("").Key("raw").String())
    	fmt.Println(cfg.Section("").Key("username").String())
    	fmt.Println(cfg.Section("").Key("string").String())
    	fmt.Println(cfg.Section("").Key("close").String())
    
    	err = cfg.Append(io.NopCloser(bytes.NewReader([]byte("append=append"))))
    	if err != nil {
    		log.Fatal("Append error:", err)
    	}
    
    	fmt.Println(cfg.Section("").Key("append").String())
    }
    

    io.NopCloser函数(no optiontion closer)是将没有Close方法的Reader添加Close方法(转换成实现ReadCloser接口的Reader),只不过是为了防止向bytes.NewReaderstrings.NewReader这样的Reader没有Close方法,底层在自动关闭的时候出错。没有关闭操作的Reader,关闭时没任何操作,有的调用自身的Close方法。

    运行程序输出:

    1
    2
    3
    4
    5
    6
    
    $ go run main.go
    原始数据
    arlettebrook
    noClose
    have closer
    append
    
  • 还可以创建一个没有任何数据源的文件对象。调用Empty函数。

    1
    
    cfg := ini.Empty()
    
  • 调用用LooseLoad的函数加载文件对象,若指定的文件不存在,不会返回错误。Load会返回错误。

    • 更牛逼的是,当那些之前不存在的文件在重新调用 Reload() 方法的时候突然出现了,那么它们会被正常加载。

      • 源码是:创建文件对象的时候会加载一次,创建完毕之后又会加载一次。
      1
      
      cfg, err := ini.LooseLoad("filename", "filename_404")
      
  • 默认情况下,当多个数据源中有相同的键时,后面的数据源会覆盖前面的数据源。

    • 调用 ShadowLoad函数,创建的数据源不会覆盖存在的值。

自定义加载ini文件对象

实现上调用LoadLooseLoadInsensitiveLoad后面会介绍)、ShadowLoad加载不同配置的文件对象,底层都是调用LoadSources(opts LoadOptions, source interface{}, others ...interface{})函数实现的。不同的配置是通过LoadOptions配置的。后面的参数都是数据源,默认必须有一个数据源:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func Load(source interface{}, others ...interface{}) (*File, error) {
	return LoadSources(LoadOptions{}, source, others...)
}

func LooseLoad(source interface{}, others ...interface{}) (*File, error) {
	return LoadSources(LoadOptions{Loose: true}, source, others...)
}

func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) {
	return LoadSources(LoadOptions{Insensitive: true}, source, others...)
}

func ShadowLoad(source interface{}, others ...interface{}) (*File, error) {
	return LoadSources(LoadOptions{AllowShadows: true}, source, others...)
}

为了方便使用,都将不同的配置封装到了不同的函数。

所以利用LoadSources我们可以实现自定义加载不同配置的文件对象。

加载选项LoadOptions常用的属性:

  1. Loose:是否忽略文件路径不存在的错误。

  2. Insensitive:是否启用不敏感加载,作用:忽略键名的大小写。底层是将键都转换为小写。键名包括分区名。

  3. AllowShadows:是否不覆盖存在键的值。开启不覆盖之后,可以调用ValueWithShadows方法,获取指定分区下所有的重复键的值。

  4. UnescapeValueDoubleQuotes:是否强制忽略键值两端的双引号。用在多个双引号的值中。

  5. SkipUnrecognizableLines:是否跳过无法识别的行。默认无法识别就会报错。

  6. IgnoreContinuation:是否忽略连续换行。就是键值不支持换换行写\

  7. UnparseableSections:标记一个分区为无法解析。当获取无法解析的分区时,调用Body方法会获取该分区的原始数据,未标记无法获取,同时未标记一个无法解析的分区,解析会报错。除非开跳过无法解析的行。

  8. AllowBooleanKeys: 是否开启布尔键。开启允许只有一个键,而没有值。解析不会报错。值永远为true。保存时也只有键。

  9. AllowPythonMultilineValues:是否允许解析多行值,用于解析换行之后对齐的字符串。

    1
    2
    3
    4
    5
    
    str = ---
        a
        b
        c
        ---
    

    开启后类似上面的字符串都可以解析。

  10. IgnoreInlineComment:忽略行内注释。

  11. SpaceBeforeInlineComment:要求注释符号前必须带有一个空格

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
	cfg, err := ini.LoadSources(ini.LoadOptions{
		Loose:                     true,
		Insensitive:               true,
		UnescapeValueDoubleQuotes: true,
		AllowShadows:              true,
		IgnoreContinuation:        true,
		SkipUnrecognizableLines:   true,
		UnparseableSections:       []string{"COMMENTS"},
	}, "my.cfg", "my.ini")

	if err != nil {
		log.Fatal(err)
	}
	body := cfg.Section("COMMENTS").Body()
	fmt.Println(body) // <1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>

my.cfg:

1
2
[COMMENTS]
<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>

注意事项

默认情况下,本库会在您进行读写操作时采用锁机制来确保数据时间。但在某些情况下,您非常确定只进行读操作。此时,您可以通过设置 cfg.BlockMode = false 来将读操作提升大约 50-70% 的性能。


操作分区(Section)

获取分区

  • 在加载配置之后,可以通过Sections方法获取所有分区对象,是切片类型的*Section对象,SectionStrings()方法获取所有分区名

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    package main
    
    import (
    	"fmt"
    	"log"
    
    	"gopkg.in/ini.v1"
    )
    
    func main() {
    	cfg, err := ini.Load("my.ini")
    	if err != nil {
    		log.Fatal("Fail to read file: ", err)
    	}
    
    	sections := cfg.Sections()
    	sectionStrings := cfg.SectionStrings()
    
    	for k, v := range sections {
    		fmt.Printf("section%v: %s\n", k+1, v.Name())
    	}
    	fmt.Print("sections:", sectionStrings)
    }
    

    运行输出 3 个分区:

    1
    2
    3
    4
    5
    
    $ go run main.go 
    section1: DEFAULT
    section2: mysql
    section3: redis
    sections:[DEFAULT mysql redis]
    
  • 调用GetSection(name)获取指定分区,如果分区不存在,会返回错误信息。返回的分区为nil

    • 但调用Section(name)会获取指定分区,如果该分区不存在,则会自动创建指定空分区并返回:

    • 示例如下:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      
      package main
      
      import (
          "fmt"
          "log"
      
          "gopkg.in/ini.v1"
      )
      
      func main() {
          cfg, err := ini.Load("my.ini")
          if err != nil {
             log.Fatal("Fail to read file: ", err)
          }
      
          newSection, err := cfg.GetSection("new")
          if err != nil {
             fmt.Println(err)
          }
          fmt.Println(newSection)
      
          fmt.Println(cfg.SectionStrings())
      
          newSection = cfg.Section("new")
      
          fmt.Println(newSection)
          fmt.Println(cfg.SectionStrings())
      }
      

      创建之后调用SectionStrings方法,新分区也会返回:

      1
      2
      3
      4
      5
      6
      
      $ go run main.go 
      section "new" does not exist
      <nil>
      [DEFAULT mysql redis]
      &{0xc000152000  new map[] [] map[] false }
      [DEFAULT mysql redis new]
      
  • 也可以手动创建一个新分区,如果分区已存在,则返回存在的分区:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
    package main
    
    import (
    	"fmt"
    	"log"
    
    	"gopkg.in/ini.v1"
    )
    
    func main() {
    	cfg, err := ini.Load("my.ini")
    	if err != nil {
    		log.Fatal("Fail to read file: ", err)
    	}
    
    	fmt.Println(cfg.SectionStrings())
    
    	mysqlSection, err := cfg.NewSection("mysql")
    	if err != nil {
    		fmt.Println(err)
    	}
    	fmt.Println(mysqlSection.Keys())
    
    	fmt.Println(cfg.SectionStrings())
    
    	newSection, err := cfg.NewSection("new")
    	if err != nil {
    		fmt.Println(err)
    	}
    	fmt.Println(newSection.Keys())
    
    	fmt.Println(cfg.SectionStrings())
    
    }
    

    运行输出:

    1
    2
    3
    4
    5
    6
    
    $ go run main.go 
    [DEFAULT mysql redis]
    [127.0.0.1 3306 root 123456 awesome]
    [DEFAULT mysql redis]
    []
    [DEFAULT mysql redis new]
    

读取父子分区

递归读取键值

在获取所有键值的过程中,特殊语法 %(<name>)s 会被应用,其中 <name> 可以是相同分区或者默认分区下的键名。字符串 %(<name>)s 会被相应的键值所替代,如果指定的键不存在,则会用空字符串替代(我测试是保留字符串)。您可以最多使用 99 层的递归嵌套。

在ini配置文件中,可以使用占位符%(name)s表示用之前已定义的键name的值来替换,这里的s表示值为字符串类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# parent_child.ini
NAME = ini
VERSION = v1
IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
USERNAME = arlettebrook

[package]
CLONE_URL = https://%(IMPORT_PATH)s

[package.sub]

上面在默认分区中设置IMPORT_PATH的值时,使用了前面定义的NAMEVERSION。 在package分区中设置CLONE_URL的值时,使用了默认分区中定义的IMPORT_PATH

我们还可以在分区名中使用.表示两个或多个分区之间的父子关系,例如package.sub的父分区为packagepackage没有父分区。 如果某个键在子分区中不存在,则会在它的父分区中再次查找,直到没有父分区为止

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
    "fmt"
    "log"

    "gopkg.in/ini.v1"
)

func main() {
    cfg, err := ini.Load("parent_child.ini")
    if err != nil {
       log.Fatal("Fail to read file: ", err)
    }

    fmt.Println("Clone url from package.sub:", cfg.Section("package.sub").Key("CLONE_URL").String())
    fmt.Println("package没有父分区:", cfg.Section("package").Key("USERNAME").String() == "")
}

运行程序输出:

1
2
3
$ go run main.go 
Clone url from package.sub: https://gopkg.in/ini.v1
package没有父分区: true

子分区中package.sub中没有键CLONE_URL,返回了父分区package中的值。

package分区中没有USERNAME,它并没有父分区,所以返回空字符串。(调用Key方法如果键不存在,会创建该键,值为空字符串。)后面会介绍


操作键(Key)

获取键

  • 在指定分区调用GetKey方法,可以获取指定的键。如果键不存在,会返回Error对象和nil。
    • 和分区一样,也可以直接获取键而忽略错误处理,调用Key方法获取指定的键,如果键不存在,会创建该键,值为空字符串。

示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
    "fmt"
    "log"

    "gopkg.in/ini.v1"
)

func main() {
    cfg, err := ini.Load("my.ini")
    if err != nil {
       log.Fatal("Fail to read file: ", err)
    }

    key, err := cfg.Section(ini.DefaultSection).GetKey("app_name123")
    if err != nil {
       fmt.Println(err)
    }
    fmt.Println(key)

    nothingness := cfg.Section("mysql").Key("app_name123")
    fmt.Println(nothingness.String() == "")

}

运行程序输出:

1
2
3
4
$ go run main.go                                 
error when getting key of section "DEFAULT": key "app_name123" not exists
<nil>
true 

默认分区中,不存在app_name123所以GetKy返回Error和nil。而Key方法返回值为空字符串的*Key类型。

键的其他操作

  • 在某个分区下,调用HasKey方法,能判断该键是否存在

  • 在某个分区下,调用NewKey方法,能够在指定分区下创建键,有两个参数,第一个:键名,第二个:值。

    • 这与创建分区不一样,分区如果存在,会返回存在的分区。
    • 键如果存在,会覆盖值。
  • 在某个分区下,调用Keys方法,能够获取指定分区下所有的*Key对象,是[]*Key类型。

    • SectionStrings方法差不多,调用KeyStrings方法,能够获取所有的键名,是[]string类型。
  • 在某分区下,调用KeysHash方法,能够获取该分区下的所有键值对的map集合。键和值的类型都为string。

    示例如下:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    package main
    
    import (
        "fmt"
        "log"
    
        "gopkg.in/ini.v1"
    )
    
    func main() {
        cfg, err := ini.Load("my.ini")
        if err != nil {
           log.Fatal("Fail to read file: ", err)
        }
    
        key := cfg.Section(ini.DefaultSection).HasKey("app_name")
    
        if key {
           fmt.Println(cfg.Section(ini.DefaultSection).Key("app_name").String())
        }
    
        newKey, err := cfg.Section(ini.DefaultSection).NewKey("app_name", "awesome go")
        if err != nil {
           log.Fatal(err)
        }
        fmt.Println(newKey.String())
    
        fmt.Printf("%#v\n", cfg.Section("").Keys())
        fmt.Println(cfg.Section("").KeyStrings())
    
        keysHash := cfg.Section("").KeysHash()
    
        for k, v := range keysHash {
           fmt.Printf("%s=%s\n", k, v)
        }
    }
    

    运行程序输出:

    1
    2
    3
    4
    5
    6
    7
    
    $ go run main.go 
    awesome web
    awesome go
    []*ini.Key{(*ini.Key)(0xc00010a690), (*ini.Key)(0xc00010a700)}
    [app_name log_level]
    app_name=awesome go
    log_level=DEBUG
    
  • 获取上级父分区下所有的键对象。

    1
    
    cfg.Section("package.sub").ParentKeys() 
    
  • 当键名为-表示自增键名,在程序中是从#1开始,#number表示,分区之间是相互独立的。

    1
    2
    3
    4
    
    [features]
    -: Support read/write comments of keys and sections
    -: Support auto-increment of key names
    -: Support load multiple files to overwrite key values
    
    1
    
    cfg.Section("features").KeyStrings()    // []{"#1", "#2", "#3"}
    

忽略键名的大小写

  • 默认情况下分区名和键名都区分大小写,当调用ini.InsensitiveLoad方法加载配置文件时,能够将所有分区和键名在读取里强制转换为小写,这样当在获取分区或者键的时候,所指定的分区名或键名不区分大小写:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    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")
    
    • 为什么在加载的时候开启转换为小写,在调用的时候就能忽略大小?
      • 因为在调用的时候会判断是否开启转换为小写,是会将查询的分区名或键名强制转换为小写。都转换为小写了,也就能够获取了。


操作键值(Value)

获取一个类型为字符串(string)的值:

1
val := cfg.Section("").Key("key name").String()

获取值的同时通过自定义函数进行处理验证:

1
2
3
4
5
6
val := cfg.Section("").Key("key name").Validate(func(in string) string {
    if len(in) == 0 {
        return "default"
    }
    return in
})

如果您不需要任何对值的自动转变功能(例如递归读取),可以直接获取原值(这种方式性能最佳):

1
val := cfg.Section("").Key("key name").Value()

判断某个原值是否存在:

1
yes := cfg.Section("").HasValue("test value")

获取其它类型的值调用对应类型的方法。返回值带有Error信息,如果不需要Error信息可以调用MustXxx方法。该方法可以指定默认值,用于转换失败的默认值。没有指定默认值为对应类型的零值。

1
2
3
4
5
6
7
8
v, err = cfg.Section("").Key("INT").Int()
v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339)
v, err = cfg.Section("").Key("TIME").Time() // RFC3339

v = cfg.Section("").Key("INT").MustInt()
v = cfg.Section("").Key("INT").MustInt(10)
v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now())
v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339
  • """"""包裹的多行字符串跟普通的获取方式一样。

  • \:一行写不下换行写,也是跟普通的获取方式一样,只不过属性IgnoreContinuation,可以忽略连续换行。就是\不起作用。

  • 默认情况下字符串中只有两端有引号,无论是单、双、三,都会自动剔除。但当字符串里面有与两端相同的引号,那么引号都会保留。

    • UnescapeValueDoubleQuotes属性会移除两端的双引号,只能是双引号。
  • 获取值的时候我们还可以指定候选。如果配置文件中的值不是候选中的值,那么将选用默认值,默认值可以不是候选里面的值。string类型是In方法,其他的是InXxx方法

    1
    2
    
    v := cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"})
    v = cfg.Section("").Key("INT").InInt(10, []int{10, 20, 30})
    
  • 验证获取的值是否在指定范围内:有三个参数:第一个:没有在范围内的默认值。第二个:最小值。第三个:最大值。string类型没有范围。

    1
    2
    3
    4
    
    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("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime)
    vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
    
  • 自动分割键值到切片(slice)。作用:获取一个键的多个值。方法是对应类型加s,并指定分隔符。

    • 当存在无效输入时,使用零值代替。
    • 注意分隔符不能为空字符串,会出现死循环。可以为空格。
    • 当在前面加上ValidXxxs,存在无效输入时,会忽略掉。
    • 当在前面加上StrictXxxs,存在无效输入时,直接返回错误。
    1
    2
    3
    
    vals = cfg.Section("").Key("INTS").Ints(",")
    vals = cfg.Section("").Key("INTS").ValidInts(",")
    vals = cfg.Section("").Key("INTS").StrictInts(",")
    
  • 修改键的值,调用SetValue方法。

    1
    2
    
    username := cfg.Section("").Key("username")
    username.SetValue("Mark")
    
  • 在某分区下调用NewBooleanKey方法,会创建布尔键,值永远为true。保存时只有键名。解析时注意要开启AllowBooleanKeys,否则会报错。

    1
    
    key, err := sec.NewBooleanKey("skip-host-cache")
    
  • 默认情况下后面出现的键会覆盖前面存在的键,当开启AllowShadows配置选项时,就是调用ShadowLoad加载数据源。后出现的键不会覆盖前面的值。还可以通过ValueWithShadows方法获取指定分区下重复键的所有值。


操作注释(Comment)

下述几种情况的内容将被视为注释:

  1. 所有以 #; 开头的行
  2. 所有在 #; 之后的内容
  3. 分区标签后的文字 (即 [分区名] 之后的内容)

如果你希望使用包含 #; 的值,请使用 ``` 或 """ 进行包覆。

除此之外,您还可以通过 LoadOptions 完全忽略行内注释:

1
2
3
cfg, err := ini.LoadSources(ini.LoadOptions{
    IgnoreInlineComment: true,
}, "app.ini")

或要求注释符号前必须带有一个空格:

1
2
3
cfg, err := ini.LoadSources(ini.LoadOptions{
    SpaceBeforeInlineComment: true,
}, "app.ini")

在分区或者键上调用Comment属性,会获取该分区或者键的所有注释(能获取头上和后边的):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
    "fmt"

    "gopkg.in/ini.v1"
)

func main() {
    cfg, _ := ini.Load("my.ini")

    c1 := cfg.Section("mysql").Comment
    fmt.Println(c1)
    fmt.Println()
    c2 := cfg.Section("").Key("log_level").Comment
    fmt.Println(c2)
}

运行程序输出:

1
2
3
4
5
$ go run main.go
; database=mysql

# possible values: DEBUG, INFO, WARNING, ERROR, FATAL
# 这也是注释

保存配置

将配置保存到某个文件,调用SaveToSaveToIndent,第二个方法多一个参数,用于指定分区下键的缩进(除默认分区),可以是\t等:

1
2
3
// ...
err = cfg.SaveTo("my.ini")
err = cfg.SaveToIndent("my.ini", "\t")

还可以写入到任何实现 io.Writer 接口的对象中,也是提供了两个方法WriteToWriteToIndent:第二个可以指定分区下键的缩进(除默认分区):

1
2
3
// ...
cfg.WriteTo(writer)
cfg.WriteToIndent(writer, "\t")

默认情况下,空格将被用于对齐键值之间的等号以美化输出结果,以下代码可以禁用该功能:

1
ini.PrettyFormat = false

下面我们通过程序生成前面使用的配置文件my.ini并保存:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
    "fmt"
    "os"

    "gopkg.in/ini.v1"
)

func main() {
    cfg := ini.Empty()

    defaultSection := cfg.Section("")
    defaultSection.NewKey("app_name", "awesome web")
    defaultSection.NewKey("log_level", "DEBUG")

    mysqlSection, err := cfg.NewSection("mysql")
    if err != nil {
       fmt.Println("new mysql section failed:", err)
       return
    }
    mysqlSection.NewKey("ip", "127.0.0.1")
    mysqlSection.NewKey("port", "3306")
    mysqlSection.NewKey("user", "root")
    mysqlSection.NewKey("password", "123456")
    mysqlSection.NewKey("database", "awesome")

    redisSection, err := cfg.NewSection("redis")
    if err != nil {
       fmt.Println("new redis section failed:", err)
       return
    }
    redisSection.NewKey("ip", "127.0.0.1")
    redisSection.NewKey("port", "6381")

    err = cfg.SaveTo("my.ini")
    if err != nil {
       fmt.Println("SaveTo failed: ", err)
    }

    err = cfg.SaveToIndent("my-pretty.ini", "\t")
    if err != nil {
       fmt.Println("SaveToIndent failed: ", err)
    }

    cfg.WriteTo(os.Stdout)
    fmt.Println()
    cfg.WriteToIndent(os.Stdout, "\t")
}

运行程序,生成两个文件my.inimy-pretty.ini,同时控制台输出文件内容。

my.ini

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
app_name  = awesome web
log_level = DEBUG

[mysql]
ip       = 127.0.0.1
port     = 3306
user     = root
password = 123456
database = awesome

[redis]
ip   = 127.0.0.1
port = 6381

my-pretty.ini

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
app_name  = awesome web
log_level = DEBUG

[mysql]
	ip       = 127.0.0.1
	port     = 3306
	user     = root
	password = 123456
	database = awesome

[redis]
	ip   = 127.0.0.1
	port = 6381

*Indent方法会对默认分区以外分区下的键增加缩进,看起来美观一点。


分区与结构体字段映射

映射到结构体

  • 调用MapTo函数或者方法,可以将文件对象映射到结构体。

    • 当MapTo为方法时,对象是文件对象或分区,参数是要映射的结构体。
    • 为了使用方便,直接将MapTo封装成了函数,该函数接收两个参数,第一个参数:结构体。第二个:数据源。
    • 当对象为分区时,映射到一个分区
    • 创建结构体的时候可以指定默认值,如果数据源没有或类型解析错误将使用默认值

    示例如下:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    
    package main
    
    import (
        "fmt"
        "log"
    
        "gopkg.in/ini.v1"
    )
    
    type Config struct {
        AppName  string `ini:"app_name"`
        LogLevel string `ini:"log_level"`
    
        MySQL MySQLConfig `ini:"mysql"`
        Redis RedisConfig `ini:"redis"`
    }
    
    type MySQLConfig struct {
        IP       string `ini:"ip"`
        Port     int    `ini:"port"`
        User     string `ini:"user"`
        Password string `ini:"password"`
        Database string `ini:"database"`
    }
    
    type RedisConfig struct {
        IP   string `ini:"ip"`
        Port int    `ini:"port"`
    }
    
    func main() {
        cfg, err := ini.Load("my.ini")
        if err != nil {
           log.Fatal("load my.ini failed: ", err)
        }
    
        assertMapToError := func(e error) {
           if e != nil {
              log.Fatal("MapTo error:", err)
           }
        }
        c1 := new(Config)
        err = cfg.MapTo(&c1)
        assertMapToError(err)
        fmt.Println(c1)
    
        c2 := new(Config)
        err = ini.MapTo(c2, "my.ini")
        assertMapToError(err)
        fmt.Println(c2)
    
        m := &MySQLConfig{
           IP: "localhost",
        }
        err = cfg.Section("mysql").MapTo(m)
        assertMapToError(err)
        fmt.Println(m)
    }
    

    MapTo内部使用了反射,所以结构体字段必须都是导出的。如果键名与字段名不相同,那么需要在结构标签中指定对应的键名。 这一点与 Go 标准库encoding/jsonencoding/xml不同。标准库json/xml解析时可以将键名app_name对应到字段名AppName。而go-ini需要[自定义键名映射器](#键名映射器(Name Mapper))才能实现这种效果。

    运行程序输出:

    1
    2
    3
    4
    
    $ go run main.go
    &{awesome web DEBUG {127.0.0.1 3306 root 123456 awesome} {127.0.0.1 6381}}
    &{awesome web DEBUG {127.0.0.1 3306 root 123456 awesome} {127.0.0.1 6381}}
    &{127.0.0.1 3306 root 123456 awesome}
    

从结构体反射

  • 我们可以调用ReflectFrom函数或方法,将结构体反射成文件对象。

    • 当为方法时,对象是反射到的文件对象或分区,参数是结构体。

      • 为了使用方便,将其封装成了函数,接收两个参数。第一个:反射到的文件对象,第二个:结构体
    • 当对象为分区时,反射到分区。

    • 注意当结构体字段与配置键不同名时需要用结构体标签指定。

      • 支持的标签:

        • ini:指定键名,或者分区名。

          • 有第二个参数omitempty,用分隔开。值为空时,省略掉,不写入文件对象。

          • 有第三参数allowshadow,如果不需要前两个标签规则,可以使用 ini:",,allowshadow" 进行简写。

            • 作用:将一个键的不同值分行保存,不用分隔符分开。
            1
            2
            3
            4
            
            [IP]
            value = 192.168.31.201
            value = 192.168.31.211
            value = 192.168.31.221
            
            1
            2
            3
            
            type IP struct {
               Value    []string `ini:"value,omitempty,allowshadow"`
            }
            
        • comment:指定注释,保存到配置注释会在键的头上。

        • delim:指定分隔符。一个键存在多个值的情况,需要指定分隔符。

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      
      type Embeded struct {
          Dates  []time.Time `delim:"|" comment:"Time data"`
          Places []string    `ini:"places,omitempty"`
          None   []int       `ini:",omitempty"`
      }
      
      ...
      Embeded{
                  []time.Time{time.Now(), time.Now()},
                  []string{"HangZhou", "Boston"},
                  []int{},
              }
      ...
      
      1
      2
      3
      4
      5
      
      ; Embeded section
      [Embeded]
      ; Time data
      Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00
      places = HangZhou,Boston
      

    示例如下:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    
    package main
    
    import (
        "fmt"
        "log"
    
        "gopkg.in/ini.v1"
    )
    
    type Config struct {
        AppName  string `ini:"app_name"`
        LogLevel string `ini:"log_level"`
    
        MySQL MySQLConfig `ini:"mysql"`
        Redis RedisConfig `ini:"redis"`
    }
    
    type MySQLConfig struct {
        IP       string `ini:"ip"`
        Port     int    `ini:"port"`
        User     string `ini:"user"`
        Password string `ini:"password"`
        Database string `ini:"database"`
    }
    
    type RedisConfig struct {
        IP   string `ini:"ip"`
        Port int    `ini:"port"`
    }
    
    func main() {
        cfg1 := ini.Empty()
    
        c1 := Config{
           AppName:  "awesome web",
           LogLevel: "DEBUG",
           MySQL: MySQLConfig{
              IP:       "127.0.0.1",
              Port:     3306,
              User:     "root",
              Password: "123456",
              Database: "awesome",
           },
           Redis: RedisConfig{
              IP:   "127.0.0.1",
              Port: 6381,
           },
        }
    
        assertReflectError := func(e error) {
           if e != nil {
              log.Fatal("Reflect error:", e)
           }
        }
    
        err := ini.ReflectFrom(cfg1, &c1)
        assertReflectError(err)
        fmt.Println(cfg1.Section("").Key("app_name").String())
    
        c2 := Config{
           AppName: "awesome go",
        }
        cfg2 := ini.Empty()
        err = cfg2.ReflectFrom(&c2)
        assertReflectError(err)
        fmt.Println(cfg2.Section("").Key("app_name").String())
    
        m := MySQLConfig{
           IP: "localhost",
        }
        cfg3 := ini.Empty()
        err = cfg3.Section("mysql").ReflectFrom(&m)
        assertReflectError(err)
        fmt.Println(cfg3.Section("mysql").Key("ip").String())
    }
    

    运行程序输出:

    1
    2
    3
    4
    
    $ go run main.go
    awesome web
    awesome go
    localhost
    

映射/反射的其它说明

任何嵌入的结构都会被默认认作一个不同的分区,并且不会自动产生所谓的父子分区关联:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type Child struct {
    Age string
}

type Parent struct {
    Name string
    Child
}

type Config struct {
    City string
    Parent
}

示例配置文件:

1
2
3
4
5
6
7
City = Boston

[Parent]
Name = Unknwon

[Child]
Age = 21

如果需要指定嵌入结构体是同一个分区,需要指定标签指定分区名如:ini:“Parent”。示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type Child struct {
    Age string
}

type Parent struct {
    Name string
    Child `ini:"Parent"`
}

type Config struct {
    City string
    Parent
}

示例配置文件:

1
2
3
4
5
City = Boston

[Parent]
Name = Unknwon
Age = 21

自定义键名和键值映射器

键名映射器(Name Mapper)

当我们利用结构体标签指定键名时,会觉得太麻烦。为了节省时间并简化代码,go-ini库支持类型为 NameMapper 的名称映射器,该映射器负责结构字段名分区名键名之间的映射

目前有 2 款内置的映射器:

  • AllCapsUnderscore:该映射器将字段名转换至格式 ALL_CAPS_UNDERSCORE 后再去匹配分区名和键名。
  • TitleUnderscore:该映射器将字段名转换至格式 title_underscore 后再去匹配分区名和键名。

使用方法:只需要将映射MapTo、反射ReflectFrom函数后面加上WithMapper,传惨时,传入对应映射器即可。或者给指定的文件对象指定映射器。属性是 NameMapper,示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
type Info struct {
	PackageName string
}

func main() {
	err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini"))
	// ...

	cfg, err := ini.Load([]byte("PACKAGE_NAME=ini"))
	// ...
	info := new(Info)
	cfg.NameMapper = ini.AllCapsUnderscore
	err = cfg.MapTo(info)
	// ...

	err = ini.ReflectFromWithMapper(cfg, &Info{}, ini.TitleUnderscore)
}

键值映射器(Value Mapper)

值映射器允许使用一个自定义函数自动展开值的具体内容,例如在运行时获取环境变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"fmt"
	"os"

	"gopkg.in/ini.v1"
)

type Env struct {
	Foo string `ini:"foo"`
}

func main() {
	cfg, _ := ini.Load([]byte("[env]\nfoo = ${USERNAME}\n"))
	cfg.ValueMapper = os.ExpandEnv
	env := &Env{}
	_ = cfg.Section("env").MapTo(env)
	fmt.Println(env)
}

运行程序输出:

1
2
$ go run main.go
&{Lenovo}

会输出你电脑的用户名。


总结

本文简单介绍了ini配置文件格式,内容来自互联网,仅供参考。还介绍了go-ini库,基本上参考的是其官方文档,官方文档写的非常详细,推荐去看,而且有中文。 作者是无闻,相信做 Go 开发的都不陌生。


参考

  1. ini配置文件格式
  2. go-ini GitHub 仓库
  3. go-ini 官方文档
  4. Go 每日一库之 go-ini