基于 Go 语言构建在线论坛(七):通过单例模式获取全局配置
为什么使用配置
在实际项目开发中,我们通常会将一些敏感信息或者可变信息通过配置文件进行配置,然后在应用中读取这些配置文件来获取配置信息。
将敏感信息通过配置文件读取是为了避免随着代码提交到公开库造成敏感信息的泄漏,给线上环境带来安全隐患,这些敏感信息包括数据库连接信息、第三方 SDK(比如微信、支付宝、Github)的密钥等。
将可变信息通过配置文件读取是为了避免硬编码,将经常变动的信息通过配置文件配置可以极大提高代码的可维护性,这些可变信息通常包括应用服务器监听的地址和端口号、目录路径设置、当前运行环境、超时时间等。
定义全局配置文件
接下来,我们为在线论坛这个简单的项目设置配置文件 config.json
,将一些敏感信息和可变信息提取到 JSON 配置文件中来:
{
"App": {
"Address": "0.0.0.0:8080",
"Static": "public",
"Log": "logs"
},
"Db": {
"Driver": "mysql",
"Address": "localhost:3306",
"Database": "chitchat",
"User": "root",
"Password": "root"
}
}
我们将应用相关的可变信息配置到 app
配置项,将数据库相关的敏感信息配置到 db
配置项。
注:为了保证配置文件不提交到公开仓库造成安全泄漏,不要把
config.json
文件提交到代码仓库。
通过单例模式初始化全局配置
在根目录下创建 config
目录,然后在该目录下新增 config.go
用来存放配置初始化代码:
package config
import (
"encoding/json"
"log"
"os"
"sync"
)
type App struct {
Address string
Static string
Log string
}
type Database struct {
Driver string
Address string
Database string
User string
Password string
}
type Configuration struct {
App App
Db Database
}
var config *Configuration
var once sync.Once
// 通过单例模式初始化全局配置
func LoadConfig() *Configuration {
once.Do(func() {
file, err := os.Open("config.json")
if err != nil {
log.Fatalln("Cannot open config file", err)
}
decoder := json.NewDecoder(file)
config = &Configuration{}
err = decoder.Decode(config)
if err != nil {
log.Fatalln("Cannot get configuration from file", err)
}
})
return config
}
我们定义了 Configuration
结构体以便和全局配置文件 config.json
字段进行映射,注意这里的首字母都需要大写(关于 JSON 编解码的细节可以阅读 JSON 编解码使用入门这篇教程)。
然后我们定义了一个 LoadConfig
方法以单例模式返回全局配置实例的指针,这里使用单例的原因是因为应用代码中可能多处都要获取配置值,重复加载配置文件进行 JSON 解码存在性能损耗(当然,定义 init
方法本身就可以支持全局运行一次,这里主要演示下单例模式如何实现)。在 Go 语言中,我们可以借助之前在并发编程中提到的 sync.Once
类型来实现单例模式,保证并发安全,在 once.Do
中定义的匿名函数全局只会执行一次(关于 sync.Once 类型的介绍可以阅读 sync 包使用之 sync.WaitGroup 和 sync.Once 这篇教程)。
项目代码重构
最后,我们将项目代码中相应位置的硬编码调整为通过上面方法返回的全局配置实例获取配置值。
Web 服务器启动参数
首先需要在 main.go
的入口位置初始化全局配置:
package main
import (
. "github.com/xueyuanjun/chitchat/config"
. "github.com/xueyuanjun/chitchat/routes"
"log"
"net/http"
)
func main() {
startWebServer()
}
// 通过指定端口启动 Web 服务器
func startWebServer() {
// 在入口位置初始化全局配置
config := LoadConfig()
r := NewRouter() // 通过 router.go 中定义的路由器来分发请求
// 处理静态资源文件
assets := http.FileServer(http.Dir(config.App.Static))
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", assets))
http.Handle("/", r)
log.Println("Starting HTTP service at " + config.App.Address)
err := http.ListenAndServe(config.App.Address, nil)
if err != nil {
log.Println("An error occured starting HTTP listener at " + config.App.Address)
log.Println("Error: " + err.Error())
}
}
我们在 startWebServer
方法的入口位置初始化全局配置,并且全局配置实例只在这里进行一次初始化,后续不会再执行加载配置文件和 JSON 解码操作,而是直接返回对应的 config
实例:
config := LoadConfig()
然后将 Web 服务器的启动参数和静态资源目录都调整为通过配置值获取,这样我们后续只需要更改配置文件即可对其进行调整,而不需要修改任何代码,降低了代码维护成本。
数据库连接配置
接下来,打开 models/db.go
,将数据库连接信息调整为通过配置文件读取:
package models
import (
"crypto/rand"
"crypto/sha1"
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
. "github.com/xueyuanjun/chitchat/config"
"log"
)
var Db *sql.DB
func init() {
var err error
config := LoadConfig() // 加载全局配置实例
driver := config.Db.Driver
source := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8&parseTime=true", config.Db.User, config.Db.Password,
config.Db.Address, config.Db.Database)
Db, err = sql.Open(driver, source)
if err != nil {
log.Fatal(err)
}
return
}
...
虽然,在这里页调用了 LoadConfig()
,但是由于是单例模式,所以会直接返回 config
实例,不会再进行初始化操作,然后我们获取配置值填充对应的 sql.Open
连接配置。
整体测试
至此,我们已经完成了通过配置文件读取应用配置的代码重构,我们可以为项目编写单元测试,也可以直接通过在浏览器访问这个在线论坛项目验证重构后应用是否可以正常运行,重新启动 Web 服务器,输出如下:
表示启动服务器时读取配置信息正常,然后访问应用首页:
好了,一切还是岁月静好,对用户来说,没有任何感知后台的变动。
下一篇教程,本来学院君想介绍下应用部署作为收尾的,不过,由于本项目是基于 sausheong/gwp/chitchat 进行的二次开发,前端视图没有做任何调整,文案都是英文的,所以穿插一篇本地化教程,介绍如何对 Go Web 应用进行本地化编程。
注:本项目源码已提交到 Github:https://github.com/nonfu/chitchat。
5 Comments
init方法好像也是只执行一次
这里主要是为了演示单例模式的使用
不是好像 是就是 init 方法是编译期间发现的 并且在 main 函数之前执行 所以全局只执行一次 也因此你可以看到很多项目配置文件、数据库连接、缓存初始化这些都是在 init 方法中完成的
请教一下,init方法的执行顺序是根据导入包的顺序来决定的吗?
嗯 但是如果导入包p1里又导入了其他包含init方法的包p2 则先执行p2的init方法,依此类推。。。