Go 每日一库之 go-ini

简介

ini 是 Windows 上常用的配置文件格式。MySQL 的 Windows 版就是使用 ini 格式存储配置的。
go-ini是 Go 语言中用于操作 ini 文件的第三方库。

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

快速使用

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

$ go get gopkg.in/ini.v1

也可以使用 GitHub 上的仓库:

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

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

app_name = awesome web

# possible values: DEBUG, INFO, WARNING, ERROR, FATAL
log_level = DEBUG

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

[redis]
ip = 127.0.0.1
port = 6381

使用 go-ini 库读取:

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方法。

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

App Name: awesome web
Log Level: DEBUG
MySQL IP: 127.0.0.1
MySQL Port: 3306
MySQL User: dj
MySQL Password: 123456
MySQL Database: awesome
Redis IP: 127.0.0.1
Redis Port: 6381

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

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

2020/01/14 22:43:13 strconv.ParseInt: parsing "x6381": invalid syntax

Must*便捷方法

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

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 时的状态,运行程序:

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源码也比较简单:

// gopkg.in/ini.v1/key.go
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
}

分区操作

获取信息

在加载配置之后,可以通过Sections方法获取所有分区,SectionStrings()方法获取所有分区名。

sections := cfg.Sections()
names := cfg.SectionStrings()

fmt.Println("sections: ", sections)
fmt.Println("names: ", names)

运行输出 3 个分区:

[DEFAULT mysql redis]

调用Section(name)获取名为name的分区,如果该分区不存在,则自动创建一个分区返回:

newSection := cfg.Section("new")

fmt.Println("new section: ", newSection)
fmt.Println("names: ", cfg.SectionStrings())

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

names:  [DEFAULT mysql redis new]

也可以手动创建一个新分区,如果分区已存在,则返回错误:

err := cfg.NewSection("new")

父子分区

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

NAME = ini
VERSION = v1
IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s

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

[package.sub]

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

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

cfg, err := ini.Load("parent_child.ini")
if err != nil {
  fmt.Println("Fail to read file: ", err)
  return
}

fmt.Println("Clone url from package.sub:", cfg.Section("package.sub").Key("CLONE_URL").String())

运行程序输出:

Clone url from package.sub: https://gopkg.in/ini.v1

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

保存配置

有时候,我们需要将生成的配置写到文件中。例如在写工具的时候。保存有两种类型的接口,一种直接保存到文件,另一种写入到io.Writer中:

err = cfg.SaveTo("my.ini")
err = cfg.SaveToIndent("my.ini", "\t")

cfg.WriteTo(writer)
cfg.WriteToIndent(writer, "\t")

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

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

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

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将配置项赋值到结构变量的对应字段中。

package main

import (
  "fmt"

  "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 {
    fmt.Println("load my.ini failed: ", err)
  }

  c := Config{}
  cfg.MapTo(&c)

  fmt.Println(c)
}

MapTo内部使用了反射,所以结构体字段必须都是导出的。如果键名与字段名不相同,那么需要在结构标签中指定对应的键名。
这一点与 Go 标准库encoding/jsonencoding/xml不同。标准库json/xml解析时可以将键名app_name对应到字段名AppName
或许这是go-ini库可以优化的点?

先加载,再映射有点繁琐,直接使用ini.MapTo将两步合并:

err = ini.MapTo(&c, "my.ini")

也可以只映射一个分区:

mysqlCfg := MySQLConfig{}
err = cfg.Section("mysql").MapTo(&mysqlCfg)

还可以通过结构体生成配置:

cfg := ini.Empty()

c := 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,
  },
}

err := ini.ReflectFrom(cfg, &c)
if err != nil {
  fmt.Println("ReflectFrom failed: ", err)
  return
}

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

总结

本文介绍了go-ini库的基本用法和一些有趣的特性。示例代码已上传GitHub
其实go-ini还有很多高级特性。官方文档非常详细,推荐去看,而且有中文哟~
作者无闻,相信做 Go 开发的都不陌生。

参考

  1. go-ini GitHub 仓库
  2. go-ini 官方文档

我的博客

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~

本文由博客一文多发平台 OpenWrite 发布!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,639评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,093评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,079评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,329评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,343评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,047评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,645评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,565评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,201评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,338评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,014评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,701评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,194评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,685评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,345评论 2 358

推荐阅读更多精彩内容