Go 每日一庫之 go-ini

語言: CN / TW / HK

簡介

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 釋出!

分享到: