还是老样子,又是一个背景交代。
最近公司一直在搞降本增效,各种优化。期间发现一些服务的配置文件热加载经常更新失败,一番分析之后,发现是框架里使用了 viper 的文件监控和热加载的功能,在一些特殊的使用姿势的情况下,会引发更新bug。

具体场景

关于热加载的使用方式,在脱敏之后的代码大概张下面这个样子:

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "time"

    "github.com/fsnotify/fsnotify"
    "github.com/gin-gonic/gin"
    "github.com/spf13/viper"
    "github.com/willas/overseer"
)

var (
    FusingConfig cfg
)
type cfg struct {
    Test item
}
type item struct {
    List []int
}

func main() {
    serverAddr := fmt.Sprintf(":8989")
    overseer.Run(overseer.Config{
        Program: prog,
        Address: serverAddr,
        Debug:   true,
    })
}

func prog(state overseer.State) {
    confFile := "/data/demo/demo.toml"
    fmt.Println("cfgFile: ", confFile)
    LoadConfig(confFile)
    viper.WatchConfig()
    viper.OnConfigChange(func(e fsnotify.Event) {
        fmt.Println("Config file changed:", e.Op)
        LoadConfig(confFile)
    })
    err := http.Serve(state.Listener, gin.New())
    if err != nil {
        fmt.Println("server start failed", err)
    }
    //等待以上结束
    time.Sleep(time.Second)
}

func LoadConfig(cfgFile string) {
    viper.SetConfigFile(cfgFile)
    err := viper.ReadInConfig() // Find and read the config file
    if err != nil {             // Handle errors reading the config file
        fmt.Println("viper read config error", err)
        return
    }
    err = viper.Unmarshal(&FusingConfig)
    if err != nil {
        fmt.Println("viper unmarshal error", err)
        return
    }
    fByte, _ := json.MarshalIndent(FusingConfig, "", "  ")
    fmt.Println("changed file content:\n", string(fByte))
}

配置文件

其中加载的配置文件内容如下:

[test]
list=[1,2,3]

bug的表现场景是:当对list数组元素进行删减时,无法正确的更新删减后的内容

截图说明

二图胜千言:

初次加载配置内容:

删除list元素之后:

可以看到热加载之后的配置文件内容没有符合预期。

解决方法

其实关键的代码是:
err = viper.Unmarshal(&FusingConfig)
这一行。

从结果反推可以大概知道,viper在把配置文件内容反序列化到 FusingConfig 结构体的时候,并不是完全重新赋值的。换句话说,对于viper来说,这个 FusingConfig 已经存在的元素数量是不会被删减,只会进行更新操作。

所以,解决的方式也很简单,每次反序列化之前,帮它重新初始化好 FusingConfig 结构体即可:

方法一

    FusingConfig = cfg{}
    err = viper.Unmarshal(&FusingConfig)

方法二

实际上,viper框架也提供了参数可以来配置是否每次反序列化之前进行初始化操作:


err = viper.Unmarshal(&FusingConfig, func(config *mapstructure.DecoderConfig) {
    config.ZeroFields = true
})

这么想来,感觉这也不是个bug了,:)