Part1介紹
當我們為自己編寫程序時,通常會將一些重要的配置項直接寫在源代碼里,比如:服務器監聽的端口、數據庫使用的名稱和端口號、HTTP請求超時的持續時間...
但是,如果我們嘗試將這個項目開源分享給他人使用,用戶使用的數據庫的用戶名和名稱可能與你不相同,甚至你還要為他們的服務器使用另一個端口。
如果你還設置了數據庫的密碼的話,為了安全,更不可能在代碼中信息泄露出來。因此,本節,將介紹如何增加我們的 ??sports?? 應用的配置模塊。
Part2增加配置模塊
在許多的開源項目中,配置都是通過鍵值(key-value) 數據結構來處理的。在真實應用中,你經常會發現一個公開配置選項的類(或者是結構體),這個類經常會將文件解析出來,將每個選擇賦值。應用程序通常會提出命令行選項以調整配置。
2.1 定義 Configuration 接口
接下來,我們為應用程序增加配置的能力,這樣上面說的很多配置就不用在代碼文件中定義。1、創建 ??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? 接口定義了檢索配置設置的方法,支持獲取字符串 string?、數字 int?、浮點型 float64?、布爾型 bool 的值:
- GetString()
- GetInt()
- GetBool()
- GetFloat()
還有一組方法允許提供一個默認值:
- GetStringDefault()
- GetIntDefault()
- GetBoolDefault()
- GetFloatDefault()
配置數據將允許嵌套的配置部分,這個將使用 GetSection() 方法實現。
2.2 來看一個基本的 JSON 配置文件
配置可以從命令行中獲取,當然更好的方式是將配置保存在一個文件中,由應用程序自動解析。
文件的格式取決于應用程序的需求。如果你需要一個復雜的配置,有級別和層次(以 Windows 注冊表的方式)關系的話,那么你可能需要考慮 JSON、YAML 或 XML 等格式。
讓我們看一個 JSON 配置文件的例子:
{
"server": {
"host": "localhost",
"port": 80
},
"database": {
"host": "localhost",
"username": "myUsername",
"password": "abcdefgh"
}
}上面的 JSON 配置文件中定義了服務器 server 和數據庫 database 的信息。但在本文中,我們基于上一節介紹的日志功能來看,為了簡化操作,只簡單配置我們的日志和主函數的信息。
2、在 sports? 目錄下,創建一個 config.json 文件,寫入如下內容:
{
"logging": {
"level": "debug"
},
"main": {
"message": "Hello, Let's Go! Hello from the config file"
}
}這個配置文件定義了兩個配置部分,分別命名為 logging? 和 main:
- logging? 部分包含一個單一的字符串配置設置,名稱為 level
- main? 部分包含一個單一的字符串配置設置,名稱為 message
這個文件顯示了配置文件使用的基本結構,在 JSON 配置文件中,要注意引號和逗號符合 JSON 文件的格式要求,很多人經常搞錯。
2.3 實現 Configuration 接口
為了能夠實現 Configuration? 接口,我們將在 sports/config? 文件夾下創建一個 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? 結構體用 map 實現了 Configuration 接口,嵌套配置部分也同樣用 maps 表示。即上面的代碼中的:
type DefaultConfig struct {
configData map[string] interface{}
}一個單獨的配置可以通過將 section? 名稱和 setting? 名稱分開,例如:logging:level?,或者使用 map? 映射來根據鍵的名稱或者值,例如 logging 。
2.4 定義接收默認值的方法
為了處理來自配置文件的值,我們在 sports/config? 文件夾下創建一個 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 定義從配置文件加載數據的函數
在 sports/config? 文件夾下新建一個加載 JSON 數據的 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? 函數讀取一個文件的內容,將其包含的 JSON? 文件解析為一個映射,并使用該映射創建一個 DefaultConfig 的值。
關于 Go 如何處理 JSON 文件,感興趣可以搜索我之前的文章:《Go 語言入門很簡單:Go 語言解析JSON》
Part3使用 Configuration 配置系統
為了從剛剛增加的配置系統中獲取日志級別的信息,我們將回到上一節中 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()? 函數,以此來將配置設置轉換為 LogLevel 的值。
最后,我們更新 main()? 函數來加載和應用配置數據,并使用配置系統來讀取它所輸出的信息,更改 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() 函數來傳遞 Configuration 的實現,最終讀取到 log 日志級別設置。
writeMessage() 函數顯示了配置部分的使用,提供了組件所需的設置,特別是在需要多個具有不同配置的實例時,每一個設置都可以在自己的部分進行定義。
最后的項目結構如圖:

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

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