头图

Go Web实战之如何增加应用配置模块

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

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

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

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

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

参考链接:


一起开启技术漂泊之旅
专注于后端技术分享,Keep Coding, Keep Loving. 热爱文学和技术,用有趣的知识武装头脑,分享简单的快乐

混迹于江湖,江湖却没有我的影子

31 声望
7 粉丝
0 条评论
推荐阅读
Go 语言解析 JSON
本文将说明如何利用 Go 语言将 JSON 解析为结构体和数组,如果解析 JSON 的嵌入对象,如何将 JSON 的自定义属性名称映射到结构体,如何解析非结构化的 JSON 字符串。

宇宙之一粟1阅读 618评论 1

封面图
Golang 中 []byte 与 string 转换
string 类型和 []byte 类型是我们编程时最常使用到的数据结构。本文将探讨两者之间的转换方式,通过分析它们之间的内在联系来拨开迷雾。

机器铃砍菜刀22阅读 55.1k评论 1

年度最佳【golang】map详解
这篇文章主要讲 map 的赋值、删除、查询、扩容的具体执行过程,仍然是从底层的角度展开。结合源码,看完本文一定会彻底明白 map 底层原理。

去去100214阅读 11k评论 2

年度最佳【golang】GMP调度详解
Golang最大的特色可以说是协程(goroutine)了, 协程让本来很复杂的异步编程变得简单, 让程序员不再需要面对回调地狱, 虽然现在引入了协程的语言越来越多, 但go中的协程仍然是实现的是最彻底的. 这篇文章将通过分析...

去去100213阅读 11.2k评论 4

【已结束】SegmentFault 思否技术征文丨浅谈 Go 语言框架
亲爱的开发者们:我们的 11 月技术征文如期而来,这次主题围绕 「 Go 」 语言,欢迎大家来参与分享~征文时间11 月 4 日 - 11 月 27 日 23:5911 月 28 日 18:00 前发布中奖名单参与条件新老思否作者均可参加征文...

SegmentFault思否11阅读 4.7k评论 11

封面图
【Go微服务】开发gRPC总共分三步
之前我也有写过RPC相关的文章:《 Go RPC入门指南:RPC的使用边界在哪里?如何实现跨语言调用?》,详细介绍了RPC是什么,使用边界在哪里?并且用Go和php举例,实现了跨语言调用。不了解RPC的同学建议先读这篇文...

王中阳Go8阅读 3.7k评论 6

封面图
【golang】sync.WaitGroup详解
上一期中,我们介绍了 sync.Once 如何保障 exactly once 语义,本期文章我们介绍 package sync 下的另一个工具类:sync.WaitGroup。

去去100213阅读 30.1k评论 2

混迹于江湖,江湖却没有我的影子

31 声望
7 粉丝
宣传栏