乔克视界 乔克视界
首页
  • 运维
  • 开发
  • 监控
  • 安全
  • 随笔
  • Docker
  • Golang
  • Python
  • AIOps
  • DevOps
  • 心情杂货
  • 读书笔记
  • 面试
  • 实用技巧
  • 博客搭建
友链
关于
收藏
  • 分类
  • 标签
  • 归档

乔克

云原生爱好者
首页
  • 运维
  • 开发
  • 监控
  • 安全
  • 随笔
  • Docker
  • Golang
  • Python
  • AIOps
  • DevOps
  • 心情杂货
  • 读书笔记
  • 面试
  • 实用技巧
  • 博客搭建
友链
关于
收藏
  • 分类
  • 标签
  • 归档
  • Docker

  • Golang

    • Golang基础知识

    • Golang进阶知识

    • Golang常用包

    • Gin框架

      • 安装
      • gin路由
      • 请求数据参数绑定
      • gin渲染
      • 使用模板渲染
      • 静态文件的使用
      • 数据渲染
      • gin重定向
      • gin同步和异步
      • go中间件
      • 会话保持
      • 文件上传
      • JWT的简单使用
      • 模板函数
      • Swagger
      • API访问控制
      • 常见的应用中间件
      • 应用配置管理
        • 优雅停止与重启
        • 集成Casbin进行访问权限控制
    • AIOps

    • Python

    • DevOps

    • 专栏
    • Golang
    • Gin框架
    乔克
    2025-07-19
    目录

    应用配置管理

    # 问题

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

    # 解决

    # 命令行参数

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

    如下:

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

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

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

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

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

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

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

    # 系统环境变量

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

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

    # 打包进二进制文件

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

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

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

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

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

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

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

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

    # 配置热更新

    # 开源的 fsnotify

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

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

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

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

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

    # 使用 viper 开源库实现热更新

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

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

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

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

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

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

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

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

    作者:乔克

    本文链接:https://jokerbai.com

    版权声明:本博客所有文章除特别声明外,均采用 署名-非商业性-相同方式共享 4.0 国际 (CC-BY-NC-SA-4.0) 许可协议。转载请注明出处!

    上次更新: 2025/07/19, 09:17:41
    常见的应用中间件
    优雅停止与重启

    ← 常见的应用中间件 优雅停止与重启→

    最近更新
    01
    使用 Generic Webhook Trigger 触发 Jenkins 多分支流水线自动化构建
    07-19
    02
    使用Zadig从0到1实现持续交付平台
    07-19
    03
    基于Jira的运维发布平台
    07-19
    更多文章>
    Theme by Vdoing | Copyright © 2019-2025 乔克 | MIT License | 渝ICP备20002153号 |
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式