在平常项目的开发中,咱们常常会使用配置文件来保存项目的基本元数据,配置文件的类型有不少,如:JSON
、xml
、yaml
、甚至多是个纯文本格式的文件。不论是什么类型的配置数据,在某些场景下,咱们可会有热更新当前配置文件内容的需求,好比:使用Go运行的一个常驻进程,运行了一个 Web Server
服务进程。git
此时,若是配置文件发生变化,咱们如何让当前程序从新读取新的配置文件内容呢?接下来,咱们将使用以下两种方式实现配置文件的更新:github
inotify
, 监听文件修改事件。不论是哪种方式,都会用到Go语言中 goroutine
的概念,我打算使用 goroutine
新起一个协程,新协程的目的是用来接收系统信号(signal)或者监听文件被修改的事件,若是你对 goroutine
的概念不是很了解,那么建议你先查阅相关资料。json
我之因此称这种方式为手动式(Manual
),是由于文件的更新是须要咱们本身去手动告知当前依赖的运行程序:"嘿,哥们!配置文件更新啦,你得从新读一下配置内容!!",咱们告知的方式就是向当前运行程序发送一个系统信号,所以程序的大概思路以下:微信
goroutine
,用来接收信号。在 *nix
系统中规定,USR1
和USR2
均属于用户自定义信号,至于USR1
和 USR2
哪个更合,维基百科 也没有给出权威的答案,因此在这里我按约定俗称的规矩,打算使用USR1
:数据结构
若是你使用过Nginx或者Apache等Web Server,那么你对采用发送信号更新配置文件的策略确定多少有点印象。并发
在Go
语言中监听系统信号须要使用 signal
包Notify()
方法,该方法至少须要两个参数,第一个参数要求是一个系统信号类型的通道,后续参数为一个或多个须要监听的系统信号:app
import "os/signal" Notify(c chan<- os.Signal, sig ...os.Signal)
所以,咱们的代码大体以下:函数
package main import ( "os" "os/signal" "syscall" ) func main() { // 声明一个容量为1的信号通道 sig := make(chan os.Signal, 1) // 监听系统SIGUSR1发出的信号 signal.Notify(sig, syscall.SIGUSR1) }
在这里咱们建立了一个信号容量大小为1的通道(channel
),这表示,通道里最多能容纳下1个信号单元,若是当前通道里已经存在一个信号单元,此时又接收到另外一个信号须要发送到通道中,那么在发送该信号的时候程序会被阻塞,直到通道里的信号被处理掉。优化
经过这种方式,咱们能够一次精确的只处理一个信号,多个信号都须要排队的目的,这正是我想要的效果。spa
当系统信号被监听存入通道后(sig
中),接下来咱们须要处理接收到到信号,这里咱们新起的协程(goroutine),使用协程的目的是但愿后续的任务不阻塞主进程的运行,在 GO
语言中,另起一个协程是很是方便的,只须要调用关键字:go
便可:
go func(){ // 新线程 }()
咱们但愿在新协程中永不停歇的获取通道中的系统信号,代码以下:
go func() { for { select { case <-sig: // 获取通道中的信号,处理信号 } } }()
GO
语言中的select
语句,其结构有点相似于其余语言的switch
语句,但不一样的是,select
只能被用来处理 goroutine
的通信操做,而goroutine
的通信又是基于channel
来实现的,因此直白点说:select
只能用来处理通道(channel
)的操做。
当前的select
一直会处于阻塞状态,直到它的某个case
符合条件时才会执行该case
条件下的语句。而且此处咱们使用了for
循环结构,让select
语句处于一个无限循环当中,若是select
下的case
接收到一个处理的信号后,当处理结束后;因为外层for
循环的语句的做用,至关于重置了select
的状态,在没有接收到新的信号时,select
将再次被阻塞等待,循环往复。
若是你对select
语句的阻塞有疑问,咱们不妨考虑下面代码的运行状况:
for { select { case <-sig: // 获取通道中的信号,处理信号 } fmt.Println("select block test!") }
在如上的select
语句后,咱们尝试输出一行字符串,那么请问:"这行fmt.Println()
函数会在for
循环中当即运行吗?"
答案是确定的:不会!select
会阻塞调,当程序运行起来时不会有任何输出,直到case
匹配到。你不妨试试。
咱们已经准备好了信号的监听,以及信号处理的简单工做,接下来咱们须要细化信号处理阶段的代码,须要添加上加载配置文件的逻辑,咱们将演示加载一份简单的json
配置文件,文件的路径存放于/tmp/env.json
,内容比较简单,仅一个test
字段:
{ "test": "D" }
同时,咱们须要建立解析该json
格式配套的数据结构:
type configBean struct { Test string }
咱们声明了一个configBean
结构体,用来和env.json
配置文件字段一一映射,而后只要调用json.Unmarshal()
函数,咱们就能够把这份json
文件内容转为对应的Go
语言结构体内容,固然这还不够,在解析完以后咱们还须要声明一个变量来存储这份结构体数据,供程序在其余地方调用:
// 全局配置变量 var Config config type config struct { LastModify time.Time Data configBean }
此处,我并无直接把configBean
解析的json
数据赋值给全局变量,而是又包装了一层,额外声明了一个字段 LastModify
用来存储当前文件的最后一次修改时间,这样的好处在于,咱们每收到一个须要更新配置文件的信号时,咱们还须要比对当前文件的修改是否大于上一次的更新时间,固然这仅仅是一个配置优化加载的小技巧。
以下即是咱们的加载配置文件的代码,这里新增了一个loadConfig(path string)
函数,用于封装加载配置文件的全部逻辑:
// 全局配置变量 var Config *config type configBean struct { Test string } type config struct { LastModify time.Time Data configBean // 配置内容存储字段 } func loadConfig(path string) error { var locker = new(sync.RWMutex) // 读取配置文件内容 data, err := ioutil.ReadFile(path) if err != nil { return err } // 读取文件属性 fileInfo, err := os.Stat(path) if err != nil { return err } // 验证文件的修改时间 if Config != nil && fileInfo.ModTime().Before(Config.LastModify) { return errors.New("no need update") } // 解析文件内容 var configBean configBean err = json.Unmarshal(data, &configBean) if err != nil { return err } config := config{ LastModify: fileInfo.ModTime(), Data: configBean, } // 从新赋值更新配置文件 locker.Lock() Config = config locker.Unlock() return nil }
关于loadConfig()
函数咱们须要说明的是,此处咱们虽然使用了锁,可是在文件读写并没使用锁,仅在赋值阶段使用,由于在这种场景下不存在多个goroutine
同时操做同一个文件的需求,若是你所在的场景存在多个goroutine
并发写操做,那么保险起见,建议你把文件的读写最好也加上锁机制。
至此,咱们大体完成了利用监听系统信号更新配置文件的全部全部逻辑,接下来咱们来演示最终成果,演示以前咱们还需在main
函数添加一点额外代码,模拟主进程成为一个常驻进程,这里仍是使用通道,最后代码大体以下:
func main() { configPath := "/tmp/env.json" done := make(chan bool, 1) // 定义信号通道 sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGUSR1) go func(path string) { for { select { case <-sig: // 收到信号, 加载配置文件 _ := loadConfig(path) } } }(configPath) // 挂起进程,直到获取到一个信号 <-done }
最终咱们使用一张gif图片来演示最终效果:
最终的完整版代码,请在此处查看:github代码地址,而且须要说明的是,demo
中的代码还有些小细节,例如:错误的处理,信号通道的关闭等,请自行处理。
预告:鉴于文章篇幅考虑,本文中咱们只实现了第一种文件更新方式。下一篇文章中,咱们将使用第二种方式:使用inotify
监听配置文件的变化,以实现配置文件的自动更新,期待你的关注。
(360技术原创内容,转载请务必保留文末二维码,谢谢~)
关于360技术
360技术是360技术团队打造的技术分享公众号,天天推送技术干货内容
更多技术信息欢迎关注“360技术”微信公众号