Go 云原生实战:如何增加 Web 应用配置模块

举报
宇宙之一粟 发表于 2022/10/27 13:36:45 2022/10/27
【摘要】 1 介绍当我们为自己编写程序时,通常会将一些重要的配置项直接写在源代码里,比如:服务器监听的端口、数据库使用的名称和端口号、HTTP请求超时的持续时间…但是,如果我们尝试将这个项目开源分享给他人使用,用户使用的数据库的用户名和名称可能与你不相同,甚至你还要为他们的服务器使用另一个端口。如果你还设置了数据库的密码的话,为了安全,更不可能在代码中信息泄露出来。因此,本节,将介绍如何增加我们的 ...

laptopgae7ad5023_1280.png

1 介绍

当我们为自己编写程序时,通常会将一些重要的配置项直接写在源代码里,比如:服务器监听的端口、数据库使用的名称和端口号、HTTP请求超时的持续时间…

但是,如果我们尝试将这个项目开源分享给他人使用,用户使用的数据库的用户名和名称可能与你不相同,甚至你还要为他们的服务器使用另一个端口。

如果你还设置了数据库的密码的话,为了安全,更不可能在代码中信息泄露出来。因此,本节,将介绍如何增加我们的 sports 应用的配置模块。

2 增加配置模块

在许多的开源项目中,配置都是通过键值(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"
    }
}

这个配置文件定义了两个配置部分,分别命名为 loggingmain

  • 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》

3 使用 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() 函数显示了配置部分的使用,提供了组件所需的设置,特别是在需要多个具有不同配置的实例时,每一个设置都可以在自己的部分进行定义。

最后的项目结构如图:

image.png

最终,我们在终端中编译并运行我们整个代码:

$ go run .
17:20:46 INFO Hello, Let's Go! Hello from the config file

整个代码会输出并打印出配置文件中的信息,如图所示:

image.png

4 总结

本文介绍了项目配置文件的由来和重要性,并从零到一编写代码,成功在我们的 Web 项目中增加了应用配置功能。并结合上一节的日志功能进行了测试。

其实在 Go 开源项目中,有个非常著名的开源配置包:Viper ,提供针对 Go 应用项目的完整配置解决方案,帮助我们快速处理所有类型的配置需求和配置文件格式。目前 GitHub Stars 数量高达 21k,今后将在后续的文章中介绍这个项目。

image.png

今天的内容到此结束。更多配置相关的内容等着大家自行探索!

希望本文能对你有所帮助,如果喜欢本文,可以点个赞或关注。

这里是宇宙之一粟,下一篇文章见!

宇宙古今无有穷期,一生不过须臾,当思奋争。

参考链接:

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。