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,今后将在后续的文章中介绍这个项目。