问题

  • Go语言在编译时不会将配置文件这类第三方文件打包进二进制文件中
  • 它既受当前路径的影响,也会因所填写的不同而改变,并非是绝对可靠的

解决

命令行参数

在Go语言中,可以直接通过flag标准库来实现该功能。实现逻辑为,如果存在命令行参数,则优先使用命令行参数,否则使用配置文件中的配置参数。

如下:

  1. var (
  2. port string
  3. runMode string
  4. config string
  5. )
  6. func init() {
  7. // 获取命令行参数
  8. err = setupFlag()
  9. if err != nil {
  10. log.Fatalf("init.setupFlag err: %v", err)
  11. }
  12. ......
  13. }
  14. // 获取命令行参数
  15. func setupFlag() error {
  16. flag.StringVar(&port, "port", "", "启动端口")
  17. flag.StringVar(&runMode, "mode", "", "启动模式")
  18. flag.StringVar(&config, "config", "config/", "指定要使用的配置文件路径")
  19. flag.Parse()
  20. return nil
  21. }

通过上述代码,我们可以通过标准库flag读取命令行参数,然后根据其默认值判断配置文件是否存在。若存在,则对读取配置的路径进行变更,代码如下:

  1. package setting
  2. import "github.com/spf13/viper"
  3. type Setting struct {
  4. vp *viper.Viper
  5. }
  6. // 初始化配置文件的基础属性
  7. func NewSetting(configs ...string) (*Setting, error) {
  8. vp := viper.New()
  9. vp.SetConfigName("config")
  10. if len(configs) != 0 {
  11. for _, config := range configs {
  12. if config != "" {
  13. vp.AddConfigPath(config)
  14. }
  15. }
  16. } else {
  17. vp.AddConfigPath("configs/")
  18. }
  19. vp.SetConfigType("yaml")
  20. err := vp.ReadInConfig()
  21. if err != nil {
  22. return nil, err
  23. }
  24. return &Setting{vp}, nil
  25. }

接下来,对ServerSetting配置项进行覆写。如果存在,则覆盖原有的文件配置,使其优先级更高,代码如下:

  1. // 初始化配置文件
  2. func setupSetting() error {
  3. setting, err := setting2.NewSetting(strings.Split(config, ",")...)
  4. if err != nil {
  5. return err
  6. }
  7. ......
  8. if port != "" {
  9. global.ServerSetting.HttpPort = port
  10. }
  11. if runMode != "" {
  12. global.ServerSetting.RunMode = runMode
  13. }
  14. return nil
  15. }

然后在运行的时候传入参数即可:

  1. go run main.go -port=8081 -mode=debug -config=configs/

系统环境变量

可以将配置文件存放在系统自带的全局变量中,如$HOME/conf或/etc/conf中,这样做的好处是不需要重新自定义一个新的系统环境变量。

一般来说,我们会在程序中内置一些系统环境变量的读取,其优先级低于命令行参数,但高于文件配置。

打包进二进制文件

可以将配置文件这种第三方文件打包进二进制文件中,这样就不需要过度关注这些第三方文件了。但这样做是有一定代价的,因此要注意使用的应用场景,即并非所有的项目都能这样操作。

首先安装go-bindata库,安装命令如下:

  1. go get -u github.com/go-bindata/go-bindata/...

通过go-bindata库可以将数据文件转换为Go代码。例如,常见的配置文件、资源文件(如Swagger UI)等都可以打包进Go代码中,这样就可以“摆脱”静态资源文件了。接下来在项目根目录下执行生成命令:

  1. go-bindata -o configs/config.go -pkg-configs configs/config.yaml

执行这条命令后,会将 configs/config.yaml 文件打包,并通过-o 选项指定的路径输出到configs/config.go文件中,再通过设置的-pkg选项指定生成的packagename为configs,接下来只需执行下述代码,就可以读取对应的文件内容了:

  1. b,_:=configs.Asset("configs/config.yaml")

把第三方文件打包进二进制文件后,二进制文件必然增大,而且在常规方法下无法做文件的热更新和监听,必须要重启并且重新打包才能使用最新的内容,因此这种方式是有利有弊的。

配置热更新

开源的fsnotify

既然要做配置热更新,那么首先要知道配置是什么时候修改的,做了哪些事?因此我们需要对所配置的文件进行监听,只有监听到了,才能知道它做了哪些变更。开源库 fsnotify 是用Go语言编写的跨平台文件系统监听事件库,常用于文件监听,因此我们可以借助该库来实现这个功能。

(1)安装
  1. go get -u golang.org/x/sys/...
  2. go get -u github.com/fsnotify/fsnotify

fsnotify是基于golang.org/x/sys实现的,并非syscall标准库,因此在安装的同时需要更新其版本,确保版本是最新的。

(2)案例
  1. package main
  2. import (
  3. "gopkg.in/fsnotify.v1"
  4. "log"
  5. )
  6. func main() {
  7. watcher, _ := fsnotify.NewWatcher()
  8. defer watcher.Close()
  9. done := make(chan bool)
  10. go func() {
  11. for {
  12. select {
  13. case event, ok := <-watcher.Events:
  14. if !ok {
  15. return
  16. }
  17. log.Fatal("event: ", event)
  18. if event.Op&fsnotify.Write == fsnotify.Write {
  19. log.Println("modified file:", event.Name)
  20. }
  21. case err, ok := <-watcher.Errors:
  22. if !ok {
  23. return
  24. }
  25. log.Fatal("error:", err)
  26. }
  27. }
  28. }()
  29. path := "configs/config.yaml"
  30. _ = watcher.Add(path)
  31. <-done
  32. }

通过监听,我们可以很便捷地知道文件做了哪些变更。进一步来说,我们可以通过对其进行二次封装,在它的上层实现一些变更动作来完成配置文件的热更新。

使用viper开源库实现热更新

viper开源库能够很便捷地实现对文件的监听和热更新。打开pkg/setting/section.go文件,针对重载应用配置项,新增如下处理方法:

  1. var sections = make(map[string]interface{})
  2. // 解析配置文件
  3. func (s *Setting) ReadSection(k string, v interface{}) error {
  4. err := s.vp.UnmarshalKey(k, v)
  5. if err != nil {
  6. return err
  7. }
  8. if _,ok:=sections[k];!ok{
  9. sections[k] = v
  10. }
  11. return nil
  12. }

首先修改ReadSection方法,增加读取section的存储记录,以便在重新加载配置的方法中进行二次处理。接下来新增ReloadAllSection方法,重新读取配置,代码如下:

  1. // 读取所有配置
  2. func (s *Setting) ReadAllSections() error {
  3. for k, v := range sections {
  4. err := s.ReadSection(k, v)
  5. if err != nil {
  6. return err
  7. }
  8. }
  9. return nil
  10. }
  11. // 监听配置变化
  12. func (s *Setting) WatchSettingChange() {
  13. go func() {
  14. s.vp.WatchConfig()
  15. s.vp.OnConfigChange(func(in fsnotify.Event) {
  16. _ = s.ReloadAllSections()
  17. })
  18. }()
  19. }

最后在pkg/setting/setting.go文件中新增文件热更新的监听和变更处理,代码如下:

  1. // 初始化配置文件的基础属性
  2. func NewSetting(configs ...string) (*Setting, error) {
  3. vp := viper.New()
  4. vp.SetConfigName("config")
  5. for _, config := range configs {
  6. if config != "" {
  7. vp.AddConfigPath(config)
  8. }
  9. }
  10. vp.SetConfigType("yaml")
  11. err := vp.ReadInConfig()
  12. if err != nil {
  13. return nil, err
  14. }
  15. // 加入热更新
  16. s := &Setting{vp: vp}
  17. s.WatchSettingChange()
  18. return s, nil
  19. }

在上述代码中,首先在NewSetting方法中起一个协程,再在里面通过WatchConfig方法对文件配置进行监听,并在OnConfigChange方法中调用刚刚编写的重载方法ReloadAllSection来处理热更新的文件监听事件回调,这样就可以“悄无声息”地实现一个文件配置热更新了。OnConfigChange方法的回调方法形参,其实就是fsnotify。

来自:《Go语言编程之旅》