基于 Go 语言构建在线论坛增补篇:通过 Viper 读取配置文件并实现热加载
简介
之前我们在论坛项目中使用了单例模式全局加载配置文件,这样做有一个弊端,就是不支持热加载,每次修改配置文件,需要重启应用,不太灵活,所以这篇教程我们引入 Viper 重构配置读取逻辑,并支持配置文件的热加载(所谓热加载指的是配置文件修改后无需重启应用即可生效)。
Viper 是 Go 语言的完整配置解决方案,支持多个数据源和丰富的功能:
- 支持设置默认配置值
- 从 JSON、YAML、TOML、HCL 等格式配置文件读取配置值
- 支持从 OS 中读取环境变量
- 支持读取命令行参数
- 支持从远程 KV 存储系统读取配置值,包括 Etcd、Consul 等
- 可以监听配置值变化,支持热加载
配置好数据源,初始化并启动 Viper 后,就可以通过 viper.Get
获取任意数据源的配置值,非常方便,还可以调用 viper.Unmarshal
方法将配置值映射到指定结构体指针。
目前已经有很多知名 Go 项目在使用 Viper 作为配置解决方案,包括 Hugo、Docker Notary、Nanobox、EMC REX-Ray、BloomApi、doctl、Mercure 等。
话不多说,下面我们在论坛项目中引入 Viper 来加载配置文件。
使用 Viper 加载配置
开始之前,先安装 Viper 扩展包:
go get github.com/spf13/viper
然后,我们在 config
目录下创建 viper.go
来定义基于 Viper 的配置初始化逻辑:
package config
import (
"encoding/json"
"fmt"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/spf13/viper"
"golang.org/x/text/language"
"time"
)
var ViperConfig Configuration
func init() {
runtimeViper := viper.New()
runtimeViper.AddConfigPath(".")
runtimeViper.SetConfigName("config")
runtimeViper.SetConfigType("json")
err := runtimeViper.ReadInConfig()
if err != nil {
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}
runtimeViper.Unmarshal(&ViperConfig)
// 本地化初始设置
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
bundle.MustLoadMessageFile(ViperConfig.App.Locale + "/active.en.json")
bundle.MustLoadMessageFile(ViperConfig.App.Locale + "/active." + ViperConfig.App.Language + ".json")
ViperConfig.LocaleBundle = bundle
}
这里,我们基于项目根目录下的 config.json
作为配置文件,在 init
方法中对应的配置文件设置代码是如下这三行:
runtimeViper.AddConfigPath(".")
runtimeViper.SetConfigName("config")
runtimeViper.SetConfigType("json")
.
表示项目根目录,config
表示配置文件名(不含格式后缀),json
表示配置文件格式,然后通过 runtimeViper.ReadInConfig()
从配置文件读取所有配置,再通过 runtimeViper.Unmarshal(&ViperConfig)
将其映射到之前定义的位于 config.go
中的 Configuration
结构体变量 ViperConfig
。接下来,就可以通过 ViperConfig
变量访问系统配置了,不过,由于本地化 LocaleBundle
属性需要额外初始化,所以我们参照单例模式中的实现对其进行初始化。ViperConfig
变量对外可见,所以只要引入 config
包的地方,都可以直接访问 ViperConfig
属性来读取配置值(init
方法会自动调用并且全局执行一次,所以无需考虑性能问题)。
重构配置读取代码
接下来,我们可以重构之前业务代码中的配置读取代码,首先是 main.go
:
func startWebServer() {
r := NewRouter() // 通过 router.go 中定义的路由器来分发请求
// 处理静态资源文件
assets := http.FileServer(http.Dir(ViperConfig.App.Static))
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", assets))
http.Handle("/", r)
log.Println("Starting HTTP service at " + ViperConfig.App.Address)
err := http.ListenAndServe(ViperConfig.App.Address, nil)
if err != nil {
log.Println("An error occured starting HTTP listener at " + ViperConfig.App.Address)
log.Println("Error: " + err.Error())
}
}
然后是 handlers/helper.go
:
func init() {
// 获取本地化实例
localizer = i18n.NewLocalizer(ViperConfig.LocaleBundle, ViperConfig.App.Language)
file, err := os.OpenFile(ViperConfig.App.Log + "/chitchat.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
...
}
...
// 生成 HTML 模板
func generateHTML(writer http.ResponseWriter, data interface{}, filenames ...string) {
var files []string
for _, file := range filenames {
files = append(files, fmt.Sprintf("views/%s/%s.html", ViperConfig.App.Language, file))
}
...
}
最后是 models/db.go
:
func init() {
var err error
driver := ViperConfig.Db.Driver
source := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8&parseTime=true", ViperConfig.Db.User, ViperConfig.Db.Password,
ViperConfig.Db.Address, ViperConfig.Db.Database)
...
}
业务逻辑非常简单:取消了之前通过 LoadConfig
方法以单例模式初始化全局配置,改为直接调用 ViperConfig
对象属性获取配置值。
通过 Viper 实现热加载
但是现在配置文件依然不支持热加载,不过 Viper 提供了对应的 API 方法实现该功能,我们打开 config/viper.go
,在 init
方法最后加上如下这段代码:
func init() {
...
// 监听配置文件变更
runtimeViper.WatchConfig()
runtimeViper.OnConfigChange(func(e fsnotify.Event) {
runtimeViper.Unmarshal(&ViperConfig)
ViperConfig.LocaleBundle.MustLoadMessageFile(ViperConfig.App.Locale + "/active." + ViperConfig.App.Language + ".json")
})
}
我们通过 runtimeViper.WatchConfig()
方法监听配置文件变更(该监听会开启新的协程执行,不影响和阻塞当前协程),一旦配置文件有变更,即可通过定义在 runtimeViper.OnConfigChange
中的匿名回调函数重新加载配置文件并将配置值映射到 ViperConfig
指针,同时再次加载新的语言文件。
这样,我们就实现了在 Go 项目中通过 Viper 实现配置文件的热加载,当然,这里只是一个简单的示例,Viper 还支持更丰富的数据源和配置操作,如果你想要了解更多可以参考 Viper 官方文档,这里就不一一介绍了。
测试配置文件读取和热加载
接下来,我们启动应用:
启动成功,则表示通过 Viper 可以正确读取 App
配置,然后访问应用首页:
一切都正常,表示数据库配置读取也是 OK 的,本地化设置也正常,为了测试配置文件的热加载,我们将 App.Language
配置值设置为 en
:
{
"App": {
"Address": "0.0.0.0:8080",
"Static": "public",
"Log": "logs",
"Locale": "locales",
"Language": "en"
},
...
}
在不重启应用的情况下,刷新论坛首页:
页面变成通过英文模板渲染的了,表明配置文件热加载生效。
9 Comments
感谢有这么详细的教程,对于初学者真的是非常友好了,特别是结合了php转化思维,学到这里对go有了大致的了解,非常感谢!。
所以说 PHPer 友好嘛 还有人怼说没有用到一点云原生技术 不是我不知道 而是我更了解 PHPer 知道什么样的东西更适合入门
此处怎么热加载
下载了git之后 运行 go run main.go 首页空白是啥情况
好吧 我自己的问题,数据库中没有建表
讲的挺详细的,我也有时间去学下go.
只能修改配置文件 热加载,修改routes.go路由或handlers下代码不能实现热加载?
那不成动态解释型语言了
我把多语言功能去掉了,更新一个数据库配置文件,热更新没起作用