Go 每日一库之 go-ini
<h2 id="简介">简介</h2><p>ini 是 Windows 上常用的配置文件格式。MySQL 的 Windows 版就是使用 ini 格式存储配置的。<br>
go-ini是 Go 语言中用于操作 ini 文件的第三方库。</p>
<p>本文介绍<code>go-ini</code>库的使用。</p>
<h2 id="快速使用">快速使用</h2>
<p>go-ini 是第三方库,使用前需要安装:</p>
<pre><code>$ go get gopkg.in/ini.v1
</code></pre>
<p>也可以使用 GitHub 上的仓库:</p>
<pre><code>$ go get github.com/go-ini/ini
</code></pre>
<p>首先,创建一个<code>my.ini</code>配置文件:</p>
<pre><code>app_name = awesome web
# possible values: DEBUG, INFO, WARNING, ERROR, FATAL
log_level = DEBUG
ip = 127.0.0.1
port = 3306
user = dj
password = 123456
database = awesome
ip = 127.0.0.1
port = 6381
</code></pre>
<p>使用 go-ini 库读取:</p>
<pre><code class="language-golang">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)
}
</code></pre>
<p>在 ini 文件中,每个键值对占用一行,中间使用<code>=</code>隔开。以<code>#</code>开头的内容为注释。ini 文件是以分区(section)组织的。<br>
分区以<code></code>开始,在下一个分区前结束。所有分区前的内容属于默认分区,如<code>my.ini</code>文件中的<code>app_name</code>和<code>log_level</code>。</p>
<p>使用<code>go-ini</code>读取配置文件的步骤如下:</p>
<ul>
<li>首先调用<code>ini.Load</code>加载文件,得到配置对象<code>cfg</code>;</li>
<li>然后以分区名调用配置对象的<code>Section</code>方法得到对应的分区对象<code>section</code>,默认分区的名字为<code>""</code>,也可以使用<code>ini.DefaultSection</code>;</li>
<li>以键名调用分区对象的<code>Key</code>方法得到对应的配置项<code>key</code>对象;</li>
<li>由于文件中读取出来的都是字符串,<code>key</code>对象需根据类型调用对应的方法返回具体类型的值使用,如上面的<code>String</code>、<code>MustInt</code>方法。</li>
</ul>
<p>运行以下程序,得到输出:</p>
<pre><code>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
</code></pre>
<p>配置文件中存储的都是字符串,所以类型为字符串的配置项不会出现类型转换失败的,故<code>String()</code>方法只返回一个值。<br>
但如果类型为<code>Int/Uint/Float64</code>这些时,转换可能失败。所以<code>Int()/Uint()/Float64()</code>返回一个值和一个错误。</p>
<p>要留意这种不一致!如果我们将配置中 redis 端口改成非法的数字 x6381,那么运行程序将报错:</p>
<pre><code>2020/01/14 22:43:13 strconv.ParseInt: parsing "x6381": invalid syntax
</code></pre>
<h2 id="must便捷方法"><code>Must*</code>便捷方法</h2>
<p>如果每次取值都需要进行错误判断,那么代码写起来会非常繁琐。为此,<code>go-ini</code>也提供对应的<code>MustType</code>(Type 为<code>Init/Uint/Float64</code>等)方法,这个方法只返回一个值。<br>
同时它接受可变参数,如果类型无法转换,取参数中第一个值返回,并且该参数设置为这个配置的值,下次调用返回这个值:</p>
<pre><code class="language-golang">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)
}
}
</code></pre>
<p>配置文件还是 redis 端口为非数字 x6381 时的状态,运行程序:</p>
<pre><code>before must, get redis port error: strconv.ParseInt: parsing "x6381": invalid syntax
redis Port: 6381
after must, get redis port: 6381
</code></pre>
<p>我们看到第一次调用<code>Int</code>返回错误,以 6381 为参数调用<code>MustInt</code>之后,再次调用<code>Int</code>,成功返回 6381。<code>MustInt</code>源码也比较简单:</p>
<pre><code class="language-golang">// 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), 10)
return defaultVal
}
return val
}
</code></pre>
<h2 id="分区操作">分区操作</h2>
<h3 id="获取信息">获取信息</h3>
<p>在加载配置之后,可以通过<code>Sections</code>方法获取所有分区,<code>SectionStrings()</code>方法获取所有分区名。</p>
<pre><code class="language-golang">sections := cfg.Sections()
names := cfg.SectionStrings()
fmt.Println("sections: ", sections)
fmt.Println("names: ", names)
</code></pre>
<p>运行输出 3 个分区:</p>
<pre><code>
</code></pre>
<p>调用<code>Section(name)</code>获取名为<code>name</code>的分区,如果该分区不存在,则自动创建一个分区返回:</p>
<pre><code class="language-golang">newSection := cfg.Section("new")
fmt.Println("new section: ", newSection)
fmt.Println("names: ", cfg.SectionStrings())
</code></pre>
<p>创建之后调用<code>SectionStrings</code>方法,新分区也会返回:</p>
<pre><code>names:
</code></pre>
<p>也可以手动创建一个新分区,如果分区已存在,则返回错误:</p>
<pre><code>err := cfg.NewSection("new")
</code></pre>
<h3 id="父子分区">父子分区</h3>
<p>在配置文件中,可以使用占位符<code>%(name)s</code>表示用之前已定义的键<code>name</code>的值来替换,这里的<code>s</code>表示值为字符串类型:</p>
<pre><code class="language-parent_child.ini">NAME = ini
VERSION = v1
IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
CLONE_URL = https://%(IMPORT_PATH)s
</code></pre>
<p>上面在默认分区中设置<code>IMPORT_PATH</code>的值时,使用了前面定义的<code>NAME</code>和<code>VERSION</code>。<br>
在<code>package</code>分区中设置<code>CLONE_URL</code>的值时,使用了默认分区中定义的<code>IMPORT_PATH</code>。</p>
<p>我们还可以在分区名中使用<code>.</code>表示两个或多个分区之间的父子关系,例如<code>package.sub</code>的父分区为<code>package</code>,<code>package</code>的父分区为默认分区。<br>
如果某个键在子分区中不存在,则会在它的父分区中再次查找,直到没有父分区为止:</p>
<pre><code class="language-golang">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())
</code></pre>
<p>运行程序输出:</p>
<pre><code>Clone url from package.sub: https://gopkg.in/ini.v1
</code></pre>
<p>子分区中<code>package.sub</code>中没有键<code>CLONE_URL</code>,返回了父分区<code>package</code>中的值。</p>
<h2 id="保存配置">保存配置</h2>
<p>有时候,我们需要将生成的配置写到文件中。例如在写工具的时候。保存有两种类型的接口,一种直接保存到文件,另一种写入到<code>io.Writer</code>中:</p>
<pre><code>err = cfg.SaveTo("my.ini")
err = cfg.SaveToIndent("my.ini", "\t")
cfg.WriteTo(writer)
cfg.WriteToIndent(writer, "\t")
</code></pre>
<p>下面我们通过程序生成前面使用的配置文件<code>my.ini</code>并保存:</p>
<pre><code class="language-golang">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")
}
</code></pre>
<p>运行程序,生成两个文件<code>my.ini</code>和<code>my-pretty.ini</code>,同时控制台输出文件内容。</p>
<p><code>my.ini</code>:</p>
<pre><code>app_name= awesome web
log_level = DEBUG
ip = 127.0.0.1
port = 3306
user = root
password = 123456
database = awesome
ip = 127.0.0.1
port = 6381
</code></pre>
<p><code>my-pretty.ini</code>:</p>
<pre><code>app_name= awesome web
log_level = DEBUG
ip = 127.0.0.1
port = 3306
user = root
password = 123456
database = awesome
ip = 127.0.0.1
port = 6381
</code></pre>
<p><code>*Indent</code>方法会对子分区下的键增加缩进,看起来美观一点。</p>
<h2 id="分区与结构体字段映射">分区与结构体字段映射</h2>
<p>定义结构变量,加载完配置文件后,调用<code>MapTo</code>将配置项赋值到结构变量的对应字段中。</p>
<pre><code class="language-golang">package main
import (
"fmt"
"gopkg.in/ini.v1"
)
type Config struct {
AppName string `ini:"app_name"`
LogLevelstring `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"`
Passwordstring `ini:"password"`
Databasestring `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)
}
</code></pre>
<p><code>MapTo</code>内部使用了反射,<strong>所以结构体字段必须都是导出的</strong>。如果键名与字段名不相同,那么需要在结构标签中指定对应的键名。<br>
这一点与 Go 标准库<code>encoding/json</code>和<code>encoding/xml</code>不同。标准库<code>json/xml</code>解析时可以将键名<code>app_name</code>对应到字段名<code>AppName</code>。<br>
或许这是<code>go-ini</code>库可以优化的点?</p>
<p>先加载,再映射有点繁琐,直接使用<code>ini.MapTo</code>将两步合并:</p>
<pre><code class="language-golang">err = ini.MapTo(&c, "my.ini")
</code></pre>
<p>也可以只映射一个分区:</p>
<pre><code class="language-golang">mysqlCfg := MySQLConfig{}
err = cfg.Section("mysql").MapTo(&mysqlCfg)
</code></pre>
<p>还可以通过结构体生成配置:</p>
<pre><code class="language-golang">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
}
</code></pre>
<h2 id="总结">总结</h2>
<p>本文介绍了<code>go-ini</code>库的基本用法和一些有趣的特性。示例代码已上传GitHub。<br>
其实<code>go-ini</code>还有很多高级特性。官方文档非常详细,推荐去看,而且有中文哟~<br>
作者无闻,相信做 Go 开发的都不陌生。</p>
<h2 id="参考">参考</h2>
<ol>
<li>go-ini GitHub 仓库</li>
<li>go-ini 官方文档</li>
</ol>
<h2 id="我">我</h2>
<p>我的博客</p>
<p>欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~</p>
<p><img src="https://img2018.cnblogs.com/blog/1919725/202001/1919725-20200116065949924-216879341.jpg"></p>
<blockquote>
<p>本文由博客一文多发平台 OpenWrite 发布!</p>
</blockquote><br><br>
来源:https://www.cnblogs.com/darjun/p/12199477.html
頁:
[1]