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 {// 判断这个路径是否为1if 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:// 否则的话,返回nilreturn 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] = cconf.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]// 删除内存中对应的keydelete(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() ...}
