40

Go 每日一库之 go-ini

 4 years ago
source link: https://studygolang.com/articles/26135
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

简介

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】,共同学习,一起进步~

bMFFVjA.jpg!web

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK