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 )
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 可以看到,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
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 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
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 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
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 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
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 在 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)
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 至此,我们的配置是从 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,今后将在后续的文章中介绍这个项目。