go-yaml 就是非常通用的一个 Go 解析库
https://github.com/go-yaml/yaml
Marshal 表示序列化一个结构成为 YAML 格式;
Unmarshal 表示反序列化一个 YAML 格式文本成为一个结构;
还有一个 UnmarshalStrict 函数,表示严格反序列化,比如如果 YAML 格式文件中包含重复 key 的字段,那么使用 UnmarshalStrict 函数反序列化会出现错误。
替换配置
// replace 表示使用环境变量maps替换context中的env(xxx)的环境变量
func replace(content []byte, maps map[string]string) []byte {
if maps == nil {
return content
}
// 直接使用ReplaceAll替换。这个性能可能不是最优,但是配置文件加载,频率是比较低的,可以接受
for key, val := range maps {
reKey := "env(" + key + ")"
content = bytes.ReplaceAll(content, []byte(reKey), []byte(val))
}
return content
}
函数递归逻辑
// 查找某个路径的配置项
func searchMap(source map[string]interface{}, path []string) interface{} {
if len(path) == 0 {
return source
}
// 判断是否有下个路径
next, ok := source[path[0]]
if ok {
// 判断这个路径是否为1
if len(path) == 1 {
return next
}
// 判断下一个路径的类型
switch next.(type) {
case map[interface{}]interface{}:
// 如果是interface的map,使用cast进行下value转换
return searchMap(cast.ToStringMap(next), path[1:])
case map[string]interface{}:
// 如果是map[string],直接循环调用
return searchMap(next.(map[string]interface{}), path[1:])
default:
// 否则的话,返回nil
return nil
}
}
return nil
}
// 通过path获取某个元素
func (conf *HadeConfig) find(key string) interface{} {
...
return searchMap(conf.confMaps, strings.Split(key, conf.keyDelim))
}
读取ymal文件
// NewHadeConfig 初始化Config方法
func NewHadeConfig(params ...interface{}) (interface{}, error) {
container := params[0].(framework.Container)
envFolder := params[1].(string)
envMaps := params[2].(map[string]string)
// 检查文件夹是否存在
if _, err := os.Stat(envFolder); os.IsNotExist(err) {
return nil, errors.New("folder " + envFolder + " not exist: " + err.Error())
}
// 实例化
hadeConf := &HadeConfig{
c: container,
folder: envFolder,
envMaps: envMaps,
confMaps: map[string]interface{}{},
confRaws: map[string][]byte{},
keyDelim: ".",
lock: sync.RWMutex{},
}
// 读取每个文件
files, err := ioutil.ReadDir(envFolder)
if err != nil {
return nil, errors.WithStack(err)
}
for _, file := range files {
fileName := file.Name()
err := hadeConf.loadConfigFile(envFolder, fileName)
if err != nil {
log.Println(err)
continue
}
}
...
return hadeConf, nil
}
// 读取某个配置文件
func (conf *HadeConfig) loadConfigFile(folder string, file string) error {
conf.lock.Lock()
defer conf.lock.Unlock()
// 判断文件是否以yaml或者yml作为后缀
s := strings.Split(file, ".")
if len(s) == 2 && (s[1] == "yaml" || s[1] == "yml") {
name := s[0]
// 读取文件内容
bf, err := ioutil.ReadFile(filepath.Join(folder, file))
if err != nil {
return err
}
// 直接针对文本做环境变量的替换
bf = replace(bf, conf.envMaps)
// 解析对应的文件
c := map[string]interface{}{}
if err := yaml.Unmarshal(bf, &c); err != nil {
return err
}
conf.confMaps[name] = c
conf.confRaws[name] = bf
}
return nil
}
逻辑非常清晰。先检查配置文件夹是否存在,然后读取文件夹中的每个以 yaml 或者 yml 后缀的文件;读取之后,先用 replace 对环境变量进行一次替换;替换之后使用 go-yaml,对文件进行解析。
配置文件热更新
这个时候,是否需要重新启动一次程序再加载一次配置文件呢?这当然是没有问题的,但是更为强大的是,我们可以自动监控配置文件目录下的所有文件,当配置文件有修改和更新的时候,能自动更新程序中的配置文件信息,也就是实现配置文件热更新。这个热更新看起来很麻烦,其实在 Golang 中是非常简单的事情。我们使用 fsnotify 库能很方便对一个文件夹进行监控,当文件夹中有文件增 / 删 / 改的时候,会通过 channel 进行事件回调。这个库的使用方式很简单。大致思路就是先使用 NewWatcher 创建一个监控器 watcher,然后使用 Add 来监控某个文件夹,通过 watcher 设置的 events 来判断文件是否有变化,如果有变化,就进行对应的操作,比如更新内存中配置服务存储的 map 结构
// NewHadeConfig 初始化Config方法
func NewHadeConfig(params ...interface{}) (interface{}, error) {
...
// 监控文件夹文件
watch, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
err = watch.Add(envFolder)
if err != nil {
return nil, err
}
go func() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
for {
select {
case ev := <-watch.Events:
{
//判断事件发生的类型,如下5种
// Create 创建
// Write 写入
// Remove 删除
path, _ := filepath.Abs(ev.Name)
index := strings.LastIndex(path, string(os.PathSeparator))
folder := path[:index]
fileName := path[index+1:]
if ev.Op&fsnotify.Create == fsnotify.Create {
log.Println("创建文件 : ", ev.Name)
hadeConf.loadConfigFile(folder, fileName)
}
if ev.Op&fsnotify.Write == fsnotify.Write {
log.Println("写入文件 : ", ev.Name)
hadeConf.loadConfigFile(folder, fileName)
}
if ev.Op&fsnotify.Remove == fsnotify.Remove {
log.Println("删除文件 : ", ev.Name)
hadeConf.removeConfigFile(folder, fileName)
}
}
case err := <-watch.Errors:
{
log.Println("error : ", err)
return
}
}
}
}()
return hadeConf, nil
}
// 删除文件的操作
func (conf *HadeConfig) removeConfigFile(folder string, file string) error {
conf.lock.Lock()
defer conf.lock.Unlock()
s := strings.Split(file, ".")
// 只有yaml或者yml后缀才执行
if len(s) == 2 && (s[1] == "yaml" || s[1] == "yml") {
name := s[0]
// 删除内存中对应的key
delete(conf.confRaws, name)
delete(conf.confMaps, name)
}
return nil
}
这里注意下,由于在运行时增加了对 confMaps 的写操作,所以需要对 confMaps 进行锁设置,以防止在写 confMaps 的时候,读操作进入读取了错误信息。
// HadeConfig 表示hade框架的配置文件服务
type HadeConfig struct {
...
lock sync.RWMutex // 配置文件读写锁
...
}
// 读取某个配置文件
func (conf *HadeConfig) loadConfigFile(folder string, file string) error {
conf.lock.Lock() defer conf.lock.Unlock() ...
}
// 通过path来获取某个配置项
func (conf *HadeConfig) find(key string) interface{} {
conf.lock.RLock()
defer conf.lock.RUnlock() ...
}