github:https://github.com/go-ini/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 }