简介

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 发布!

Go 每日一库之 go-ini的更多相关文章

  1. Go 每日一库之 flag

    缘起 我一直在想,有什么方式可以让人比较轻易地保持每日学习,持续输出的状态.写博客是一种方式,但不是每天都有想写的,值得写的东西. 有时候一个技术比较复杂,写博客的时候经常会写着写着发现自己的理解有偏 ...

  2. Go 每日一库之 viper

    简介 上一篇文章介绍 cobra 的时候提到了 viper,今天我们就来介绍一下这个库. viper 是一个配置解决方案,拥有丰富的特性: 支持 JSON/TOML/YAML/HCL/envfile/ ...

  3. Go 每日一库之 fsnotify

    简介 上一篇文章Go 每日一库之 viper中,我们介绍了 viper 可以监听文件修改进而自动重新加载. 其内部使用的就是fsnotify这个库,它是跨平台的.今天我们就来介绍一下它. 快速使用 先 ...

  4. Go 每日一库之 go-flags

    简介 在上一篇文章中,我们介绍了flag库.flag库是用于解析命令行选项的.但是flag有几个缺点: 不显示支持短选项.当然上一篇文章中也提到过可以通过将两个选项共享同一个变量迂回实现,但写起来比较 ...

  5. Go 每日一库之 go-homedir

    简介 今天我们来看一个很小,很实用的库go-homedir.顾名思义,go-homedir用来获取用户的主目录. 实际上,使用标准库os/user我们也可以得到这个信息: package main i ...

  6. Go 每日一库之 cobra

    简介 cobra是一个命令行程序库,可以用来编写命令行程序.同时,它也提供了一个脚手架, 用于生成基于 cobra 的应用程序框架.非常多知名的开源项目使用了 cobra 库构建命令行,如Kubern ...

  7. go每日一库 [home-dir] 获取用户主目录

    关于我 我的博客|文章首发 顾名思义,go-homedir用来获取用户的主目录.实际上,通过使用标准库os/user我们也可以得到内容,使用以下方式 标准库使用 package main import ...

  8. [python每日一库]——hotshot

    High performance logging profiler 官方文档:http://docs.python.org/2/library/hotshot.html#module-hotshot ...

  9. mysql扩展库-1

    启用mysql扩展库 在php.ini文件中去配置mysql扩展库 extension=php_mysql.dll 可以通过 phpinfo() 查看当前php支持什么扩展库. 在sql扩展库中创建一 ...

随机推荐

  1. java表达式和三目运算符

    是由数字.运算符.数字分组符号(括号)等以能求得数值的有意义排列的序列; a + b 3.14 + a (x + y) * z + 100 boolean b= i < 10 && ...

  2. H3C ACL包过滤显示与调试

  3. HDU 6621"K-th Closest Distance"(二分+主席树)

    传送门 •题意 有 $m$ 次询问,每次询问求 $n$ 个数中, $[L,R]$ 区间距 $p$ 第 $k$ 近的数与 $p$ 差值的绝对值: •题解 二分答案,假设当前二分的答案为 $x$,那么如何 ...

  4. springSecurity安全框架的学习和原理解读

    最近在公司的项目中使用了spring security框架,所以有机会来学习一下,公司的项目是使用springboot搭建 springBoot版本1.59 spring security 版本4.2 ...

  5. java 注解(Annotation)

    注解相当于一种标记,在程序中加了注解就等于为程序打上了某种标记,没加,则等于没有某种标记. 以后,javac编译器,开发工具和其他程序可以用反射来了解你的类及各种元素上有无何种标记,看你有什么标记,就 ...

  6. 2019-4-10-win10-uwp-自定义标记扩展

    title author date CreateTime categories win10 uwp 自定义标记扩展 lindexi 2019-04-10 09:46:13 +0800 2019-04- ...

  7. WPF 解决弹出模态窗口关闭后,主窗口不在最前

    本文告诉大家如何解决这个问题,在 WPF 的软件,弹出一个模态窗口.使用另一个窗口在模态窗口前面.从任务栏打开模态窗口.关闭模态窗口.这时发现,主窗口会在刚才使用的另一个窗口下面 这是 Windows ...

  8. D3.js力导向图中新增节点及新增关系连线示例

    大家在使用D3.js中的力导向图时,基本都会遇到动态增加节点及连线的需求,这里记录一下我的实现方式. 话不多说,先放代码: <!DOCTYPE html> <html lang=&q ...

  9. Linux 内核 中断 urb

    函数 usb_fill_int_urb 是一个帮忙函数, 来正确初始化一个 urb 来发送给 USB 设备的 一个中断端点: void usb_fill_int_urb(struct urb *urb ...

  10. LuoguP2765 魔术球问题

    LuoguP2765 魔术球问题 首先,很难看出来这是一道网络流题.但是因为在网络流24题中,所以还是用网络流的思路 首先考虑完全平方数的限制. 如果\(i,j\)满足\(i < j\) 且 $ ...