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_name
和log_level
。
使用go-ini
讀取配置檔案的步驟如下:
- 首先呼叫
ini.Load
載入檔案,得到配置物件cfg
; - 然後以分割槽名呼叫配置物件的
Section
方法得到對應的分割槽物件section
,預設分割槽的名字為""
,也可以使用ini.DefaultSection
; - 以鍵名呼叫分割槽物件的
Key
方法得到對應的配置項key
物件; - 由於檔案中讀取出來的都是字串,
key
String
、MustInt
方法。
執行以下程式,得到輸出:
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
的值時,使用了前面定義的NAME
和VERSION
。
在package
分割槽中設定CLONE_URL
的值時,使用了預設分割槽中定義的IMPORT_PATH
。
我們還可以在分割槽名中使用.
表示兩個或多個分割槽之間的父子關係,例如package.sub
的父分割槽為package
,package
的父分割槽為預設分割槽。
如果某個鍵在子分割槽中不存在,則會在它的父分割槽中再次查詢,直到沒有父分割槽為止:
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.ini
和my-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/json
和encoding/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 開發的都不陌生。
參考
- go-ini GitHub 倉庫
- go-ini 官方文件
我
我的部落格
歡迎關注我的微信公眾號【GoUpUp】,共同學習,一起進步~
本文由部落格一文多發平臺 OpenWrite 釋出!