Part1介紹
當(dāng)我們?yōu)樽约壕帉懗绦驎r,通常會將一些重要的配置項直接寫在源代碼里,比如:服務(wù)器監(jiān)聽的端口、數(shù)據(jù)庫使用的名稱和端口號、HTTP請求超時的持續(xù)時間...
但是,如果我們嘗試將這個項目開源分享給他人使用,用戶使用的數(shù)據(jù)庫的用戶名和名稱可能與你不相同,甚至你還要為他們的服務(wù)器使用另一個端口。
如果你還設(shè)置了數(shù)據(jù)庫的密碼的話,為了安全,更不可能在代碼中信息泄露出來。因此,本節(jié),將介紹如何增加我們的 ??sports?
? 應(yīng)用的配置模塊。
Part2增加配置模塊
在許多的開源項目中,配置都是通過鍵值(key-value) 數(shù)據(jù)結(jié)構(gòu)來處理的。在真實應(yīng)用中,你經(jīng)常會發(fā)現(xiàn)一個公開配置選項的類(或者是結(jié)構(gòu)體),這個類經(jīng)常會將文件解析出來,將每個選擇賦值。應(yīng)用程序通常會提出命令行選項以調(diào)整配置。
2.1 定義 Configuration 接口
接下來,我們?yōu)閼?yīng)用程序增加配置的能力,這樣上面說的很多配置就不用在代碼文件中定義。1、創(chuàng)建 ??sports/config?
?? 文件夾,然后新建一個 ??config.go?
? 文件,寫入如下的代碼:
package config
type Configuration interface {
GetString(name string) (configValue string, found bool)
GetInt(name string) (configValue int, found bool)
GetBool(name string) (configValue bool, found bool)
GetFloat(name string) (configValue float64, found bool)
GetStringDefault(name, defVal string) (configValue string)
GetIntDefault(name string, defVal int) (configValue int)
GetBoolDefault(name string, defVal bool) (configValue bool)
GetFloatDefault(name string, defVal float64) (configValue float64)
GetSection(sectionName string) (section Configuration, found bool)
}
可以看到,Configuration? 接口定義了檢索配置設(shè)置的方法,支持獲取字符串 string?、數(shù)字 int?、浮點型 float64?、布爾型 bool 的值:
- GetString()
- GetInt()
- GetBool()
- GetFloat()
還有一組方法允許提供一個默認(rèn)值:
- GetStringDefault()
- GetIntDefault()
- GetBoolDefault()
- GetFloatDefault()
配置數(shù)據(jù)將允許嵌套的配置部分,這個將使用 GetSection() 方法實現(xiàn)。
2.2 來看一個基本的 JSON 配置文件
配置可以從命令行中獲取,當(dāng)然更好的方式是將配置保存在一個文件中,由應(yīng)用程序自動解析。
文件的格式取決于應(yīng)用程序的需求。如果你需要一個復(fù)雜的配置,有級別和層次(以 Windows 注冊表的方式)關(guān)系的話,那么你可能需要考慮 JSON、YAML 或 XML 等格式。
讓我們看一個 JSON 配置文件的例子:
{
"server": {
"host": "localhost",
"port": 80
},
"database": {
"host": "localhost",
"username": "myUsername",
"password": "abcdefgh"
}
}
上面的 JSON 配置文件中定義了服務(wù)器 server 和數(shù)據(jù)庫 database 的信息。但在本文中,我們基于上一節(jié)介紹的日志功能來看,為了簡化操作,只簡單配置我們的日志和主函數(shù)的信息。
2、在 sports? 目錄下,創(chuàng)建一個 config.json 文件,寫入如下內(nèi)容:
{
"logging": {
"level": "debug"
},
"main": {
"message": "Hello, Let's Go! Hello from the config file"
}
}
這個配置文件定義了兩個配置部分,分別命名為 logging? 和 main:
- logging? 部分包含一個單一的字符串配置設(shè)置,名稱為 level
- main? 部分包含一個單一的字符串配置設(shè)置,名稱為 message
這個文件顯示了配置文件使用的基本結(jié)構(gòu),在 JSON 配置文件中,要注意引號和逗號符合 JSON 文件的格式要求,很多人經(jīng)常搞錯。
2.3 實現(xiàn) Configuration 接口
為了能夠?qū)崿F(xiàn) Configuration? 接口,我們將在 sports/config? 文件夾下創(chuàng)建一個 config_default.go 文件,然后寫入如下代碼:
package config
import "strings"
type DefaultConfig struct {
configData map[string]interface{}
}
func (c *DefaultConfig) get(name string) (result interface{}, found bool) {
data := c.configData
for _, key := range strings.Split(name, ":") {
result, found = data[key]
if newSection, ok := result.(map[string]interface{}); ok && found {
data = newSection
} else {
return
}
}
return
}
func (c *DefaultConfig) GetSection(name string) (section Configuration, found bool) {
value, found := c.get(name)
if found {
if sectionData, ok := value.(map[string]interface{}); ok {
section = &DefaultConfig{configData: sectionData}
}
}
return
}
func (c *DefaultConfig) GetString(name string) (result string, found bool) {
value, found := c.get(name)
if found {
result = value.(string)
}
return
}
func (c *DefaultConfig) GetInt(name string) (result int, found bool) {
value, found := c.get(name)
if found {
result = int(value.(float64))
}
return
}
func (c *DefaultConfig) GetBool(name string) (result bool, found bool) {
value, found := c.get(name)
if found {
result = value.(bool)
}
return
}
func (c *DefaultConfig) GetFloat(name string) (result float64, found bool) {
value, found := c.get(name)
if found {
result = value.(float64)
}
return
}
DefaultConfig? 結(jié)構(gòu)體用 map 實現(xiàn)了 Configuration 接口,嵌套配置部分也同樣用 maps 表示。即上面的代碼中的:
type DefaultConfig struct {
configData map[string] interface{}
}
一個單獨(dú)的配置可以通過將 section? 名稱和 setting? 名稱分開,例如:logging:level?,或者使用 map? 映射來根據(jù)鍵的名稱或者值,例如 logging 。
2.4 定義接收默認(rèn)值的方法
為了處理來自配置文件的值,我們在 sports/config? 文件夾下創(chuàng)建一個 config_default_fallback.go 文件:
package config
func (c *DefaultConfig) GetStringDefault(name, val string) (result string) {
result, ok := c.GetString(name)
if !ok {
result = val
}
return
}
func (c *DefaultConfig) GetIntDefault(name string, val int) (result int) {
result, ok := c.GetInt(name)
if !ok {
result = val
}
return
}
func (c *DefaultConfig) GetBoolDefault(name string, val bool) (result bool) {
result, ok := c.GetBool(name)
if !ok {
result = val
}
return
}
func (c *DefaultConfig) GetFloatDefault(name string, val float64) (result float64) {
result, ok := c.GetFloat(name)
if !ok {
result = val
}
return
}
2.5 定義從配置文件加載數(shù)據(jù)的函數(shù)
在 sports/config? 文件夾下新建一個加載 JSON 數(shù)據(jù)的 config_json.go 文件,寫入如下代碼:
package config
import (
"encoding/json"
"os"
"strings"
)
func Load(filename string) (config Configuration, err error) {
var data []byte
data, err = os.ReadFile(filename)
if err == nil {
decoder := json.NewDecoder(strings.NewReader(string(data)))
m := map[string]interface{}{}
err = decoder.Decode(&m)
if err == nil {
config = &DefaultConfig{configData: m}
}
}
return
}
Load? 函數(shù)讀取一個文件的內(nèi)容,將其包含的 JSON? 文件解析為一個映射,并使用該映射創(chuàng)建一個 DefaultConfig 的值。
關(guān)于 Go 如何處理 JSON 文件,感興趣可以搜索我之前的文章:《Go 語言入門很簡單:Go 語言解析JSON》
Part3使用 Configuration 配置系統(tǒng)
為了從剛剛增加的配置系統(tǒng)中獲取日志級別的信息,我們將回到上一節(jié)中 logging 文件夾中的 default_create.go 文件中,寫入如下代碼:
package logging
import (
"log"
"os"
"strings"
"sports/config"
)
// func NewDefaultLogger(level LogLevel) Logger {
func NewDefaultLogger(cfg config.Configuration) Logger {
// 使用 Configuration
var level LogLevel = Debug
if configLevelString, found := cfg.GetString("logging:level"); found {
level = LogLevelFromString(configLevelString)
}
flags := log.Lmsgprefix | log.Ltime
return &DefaultLogger{
minLevel: level,
loggers: map[LogLevel]*log.Logger{
Trace: log.New(os.Stdout, "TRACE ", flags),
Debug: log.New(os.Stdout, "DEBUG ", flags),
Information: log.New(os.Stdout, "INFO ", flags),
Warning: log.New(os.Stdout, "WARNING ", flags),
Fatal: log.New(os.Stdout, "FATAL ", flags),
},
triggerPanic: true,
}
}
func LogLevelFromString(val string) (level LogLevel) {
switch strings.ToLower(val) {
case "debug":
level = Debug
case "information":
level = Information
case "warning":
level = Warning
case "fatal":
level = Fatal
case "none":
level = None
}
return
}
在 JSON 中沒有很好的方法來表示 iota? 值,所以我們使用一個字符串并定義了 LogLevelFromString()? 函數(shù),以此來將配置設(shè)置轉(zhuǎn)換為 LogLevel 的值。
最后,我們更新 main()? 函數(shù)來加載和應(yīng)用配置數(shù)據(jù),并使用配置系統(tǒng)來讀取它所輸出的信息,更改 main.go 文件如下。
package main
import (
// "fmt"
"sports/config"
"sports/logging"
)
// func writeMessage(logger logging.Logger) {
// // fmt.Println("Let's Go")
// logger.Info("Let's Go, logger")
// }
// func main() {
// var logger logging.Logger = logging.NewDefaultLogger(logging.Information)
// writeMessage(logger)
// }
func writeMessage(logger logging.Logger, cfg config.Configuration) {
section, ok := cfg.GetSection("main")
if ok {
message, ok := section.GetString("message")
if ok {
logger.Info(message)
} else {
logger.Panic("Cannot find configuration setting")
}
} else {
logger.Panic("Config section not found")
}
}
func main() {
var cfg config.Configuration
var err error
cfg, err = config.Load("config.json")
if err != nil {
panic(err)
}
var logger logging.Logger = logging.NewDefaultLogger(cfg)
writeMessage(logger, cfg)
}
至此,我們的配置是從 config.json? 文件中獲取,通過 NewDefaultLogger() 函數(shù)來傳遞 Configuration 的實現(xiàn),最終讀取到 log 日志級別設(shè)置。
writeMessage() 函數(shù)顯示了配置部分的使用,提供了組件所需的設(shè)置,特別是在需要多個具有不同配置的實例時,每一個設(shè)置都可以在自己的部分進(jìn)行定義。
最后的項目結(jié)構(gòu)如圖:

最終,我們在終端中編譯并運(yùn)行我們整個代碼:
$ go run .
17:20:46 INFO Hello, Let's Go! Hello from the config file
整個代碼會輸出并打印出配置文件中的信息,如圖所示:

Part4總結(jié)
本文介紹了項目配置文件的由來和重要性,并從零到一編寫代碼,成功在我們的 Web 項目中增加了應(yīng)用配置功能。并結(jié)合上一節(jié)的日志功能進(jìn)行了測試。
其實在 Go 開源項目中,有個非常著名的開源配置包:Viper ,提供針對 Go 應(yīng)用項目的完整配置解決方案,幫助我們快速處理所有類型的配置需求和配置文件格式。目前 GitHub Stars 數(shù)量高達(dá) 21k,今后將在后續(xù)的文章中介紹這個項目。