关于 Go 配置文件的最优设计方案?
看到 G02 的 3.9 小节,里面的代码非常长,花了蛮长时间看,然后有些问题想和大家一起探讨一下。
原文中的配置文件相关代码如下:
config/app.go
config/config.go
pkg/helpers/helpers.go
pkg/config/config.go
main.go
.env
总体分析一下,config 包的目的是为了封装 viper,让其他包的项目代码感知不到 viper 的存在,并且在 config 包里定义了 Get、GetString、GetInt 等方法,这样可以方便地在其他包中使用 config.Get
获取到配置项。
pkg/config 包内定义了一个 ConfigFuncs
问题 1:config 包中 Initialize 方法存在的意义?
在 main.go 中的 init 方法中调用了 btsConfig.Initialize()
,这个方法里是空的,里面什么都没有,目的只是单纯地想触发包内所有的 init 函数而已。但是包内的 init 总是会比 Initialize 先执行,也就是说,把 init 方法里的内容剪贴到 Initialize 函数里执行结果是没有任何差异的对吧?
那按我的理解,Initialize 存在的意义是省去了新增配置文件的时候(例如:新增了一个 config/cors.go 文件,用于添加跨域配置项),需要动脑筋想函数名,还要在 Initialize 中调用一次该函数的过程,是不是可以这么理解?
问题 2:为什么 pkg/config 包中定义的 ConfigFunc 不是 map[string]interface{} 类型,而是返回该类型的方法?
我觉得 config/app.go 中添加配置的方法看起来比较麻烦,是这样的:
func init() {
config.Add("app", func() map[string]interface{} {
return map[string]interface{} {
// 应用名称
"name": config.Env("APP_NAME", "Gohub"),
}
})
}
于是我就把 ConfigFunc 改成了 map[string]interface{} 类型,pkg/config 包内相关的函数参数类型也改了,config/app.go 也改成了这样:
var app = map[string]interface{} {
// 应用名称
"name": config.Env("APP_NAME", "Gohub"),
}
func init() {
config.Add("app", app)
}
然后一跑,发现 config.Get 获取不到配置项的值了!
排查了很久,发现函数执行的顺序和想象中是一样的:
2022-01-10 09:40:08.038 debug pkg/config/config.go:24 init pkg/config/config
2022-01-10 09:40:08.039 debug config/config.go:14 init config/config
2022-01-10 09:40:08.039 debug pkg/config/config.go:93 Add pkg/config/config
2022-01-10 09:40:08.048 debug config/config.go:10 Initialize config/config
2022-01-10 09:40:08.048 debug pkg/config/config.go:42 InitConfig pkg/config/config
2022-01-10 09:40:08.050 debug pkg/config/config.go:50 loadConfig pkg/config/config
// 但是接下来 config.Get("app.name") 就获取不到配置项的值了
关键在于 pkg/config/config.go 中 Add 方法和 loadConfig 方法的执行顺序:
// Add 新增配置项
func Add(name string, configFn ConfigFunc) { // 这里的 ConfigFunc 被我改成 map[string]interface{} 类型了
ConfigFuncs[name] = configFn
}
// loadConfig 载入配置项
func loadConfig() {
for name, fn := range ConfigFuncs {
viper.Set(name, fn()) // 这里的 fn() 也改成了对应的 fn,即 map 类型
}
}
// main.go
package main
func init() {
// 加载 config 目录下的配置信息
btsConig.Initialize()
}
func main() {
// 配置初始化,依赖命令行 --env 参数
var env string
flag.StringVar(&env, "env", "", "加载 .env 文件,如 --env=testing 加载的是 .env.testing 文件")
flag.Parse()
config.InitConfig(env)
// ...
}
Add 方法是由 config/config.go 的 init 调用的,config 包的初始化是 config/config.go 的 Initialize 触发的。Add 方法结束后,回溯到 Initialize 结束。接下来才是 InitConfig 和 loadConfig。那么问题来了,如果经过 loadConfig 中的 viper.Set 将配置项载入到 viper 中,那为什么在 loadConfig 之后的 config.Get 也获取不到值呢?但是从 map 改回 func 就可以?
啊,想了好久,没搞明白。传递一个闭包和传递一个值的关系有什么区别呢?唔……
问题三:有什么简化的方案?
经过分析发现这个方案几乎就是 laravel 的同款配置方案,以多维数组的形式分割为多个模块的配置,统一载入,随处获取,具有可覆盖性。但是获取配置项的时候是需要自己控制类型的,例如端口:
"port": config.Env("HTTP_PORT", 8000), // 默认值这么定义也可以
"port": config.Env("HTTP_PORT", "8000"), // 这样也可以
不管配置文件类型如何,不管给的默认值类型如何,关键在于取出来是什么类型,如果需要 Int 那就 GetInt,需要 String 那就 Get 或者 GetString,写着写着就感觉丢失了 Go 的强类型优势,而有一种在写 PHP 的错觉?
我看我的小伙伴的配置管理是这么写的,非常之简单:
这样虽然异常简单,但对于中小项目来说使用上基本足够了。
虽然我也很想用 viper,但是教程的做法有种杀鸡用牛刀的感觉。
所以想知道,如果折中一下,用 viper 解析配置文件绑定到结构体上对于系统的健壮性会不会更好一点,毕竟强类型是 Go 的优势,总担心写 Go 写成 PHP,嗐~
P.S. 这里把教程里的代码以图片的形式贴出来了,目的只是单纯地希望没有购买课程的同学也能一起看一看探讨一下我提出的问题啊!不是想盗版啊什么的啊! 🙏🏻
推荐文章: