需求:
使用viper管理配置文件。项目部署后,通过修改环境变量,以达到使用环境变量中的配置 覆盖 config file中配置的目的。
// 代码结构
################# conf/app1.yaml #################
TEST_ENV: 12345
TEST_GROUPS:
USER : user
ROLE : role
MANER : maner
pkg/setting/section.go ///
// 该文件中定义了与yaml配置文件中相对应的结构体
package setting
// TODO:mapstructure 是否可以使用‘.’来映射配置文件中的嵌套类型;如 TEST_GROUPS.USER; 【通过测试,该写法不可取】
type Groups struct {
User string `mapstructure:"USER"`
Role string `mapstructure:"ROLE"`
Maner string `mapstructure:"MANER"`
}
type Config struct {
TestEnv string `mapstructure:"TEST_ENV"`
}
pkg/setting/setting.go ///
// 在该文件中对配置文件进行初始化
package setting
type Setting struct {
vp *viper.Viper
}
var (
ConfigSetting = &Config{}
GroupsSetting = &Groups{}
)
func NewSetting() (*Setting, error) {
vp := viper.New()
vp.SetConfigName("app")
vp.AddConfigPath("conf/")
vp.SetConfigType("yaml")
vp.AddConfigPath(".")
// 设置为true 可自动获取相应的环境变量
vp.AutomaticEnv()
// 设置与本项目相关的环境变量的前缀
vp.SetEnvPrefix("app")
err := vp.ReadInConfig()
if err != nil {
return nil, err
}
return &Setting{vp}, nil
}
func Setup() error {
setting, err := NewSetting()
if err != nil {
return err
}
// 将配置文件按照 mapstructure 映射 读取到相应的变量中
err = setting.vp.Unmarshal(&ConfigSetting)
if err != nil {
return err
}
// 将配置文件 按照 父节点读取到相应的struct中
err = setting.vp.UnmarshalKey("groups", &GroupsSetting)
if err != nil {
return err
}
}
通过 EXPORT APP_TEST_ENV=000233设置环境变量,覆盖配置文件中的 TEST_ENV
main.go ///
package main
func main() {
setting.Setup()
fmt.Println(setting.ConfigSetting)
fmt.Println(setting.GroupsSetting)
}
/*
the output is:
&{000233}
&{user role maner}
*/
实验验证成功!
可直接通过 viper.Unmarshal(&ConfigSetting) 进行读取,方法执行时,如果 AutomaticEnv = true,且 viper.SetEnvPrefix(“app”) 设置成功。那么在加载配置文件的时候,会自动使用env中的配置将配置文件中的配置覆盖掉;其配置文件与env中项的对应关系为:
APP_TEST_ENV(环境变量中的配置,必须为全部大写) <=> Test_Env(此处为配置文件中的项,大小写不敏感)
总结:
在非嵌套型的配置中,可以通过设置 EnvPrefix 和 AutomaticEnv 来自动使用env覆盖config中的配置项
通过 viper.UnmarshalKey(“groups”, &GroupsSetting) 将配置文件中 groups 下的全部配置加载到 GroupsSetting 中。
若环境变量中存在了 APP_GROUPS = xxx ;此时会出现报错,因为上述方法执行的时候,会将环境变量中的配置读取出来,并替换掉配置文件中groups下的全部数据。(注意,通过env读取到的groups数据,全部为string类型的;而在配置文件中读取的数据为map型的)此时报错信息为:
'' expected a map, got 'string'
对于该问题,可以通过自定义 DecoderConfigOption 钩子函数来解决问题。// TODO 还未完成
-------------------------------------补充Hook函数解决方案----------------------------------------
源码分析
// 1. 首先,调用UnmarshalKey("groups", GroupsSetting)
err = setting.vp.UnmarshalKey("groups", GroupsSetting)
// 2. 接下来 UnmarshalKey 函数的实现为
func (v *Viper) UnmarshalKey(key string, rawVal interface{}, opts ...DecoderConfigOption) error {
return decode(v.Get(key), defaultDecoderConfig(rawVal, opts...))
}
// 2.1 通过 v.Get(key) 来构造decode函数的第一个参数
func (v *Viper) Get(key string) interface{} {
lcaseKey := strings.ToLower(key)
// 注意 此处find函数并未拿出来,但在根据key读取数据的时候,优先级为 flag > env > config file > key/value store.
// 因此,若配置同时存在于env中,那么会直接覆盖掉配置文件中的配置
val := v.find(lcaseKey, true)
if val == nil {
return nil
}
...
}
// 2.1.1 find(key string, flagDefault bool) 核心函数, 按照如下优先级读取配置
// overrides > flag > env > config > key/value store > default
func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} {
var (
val interface{}
exists bool
path = strings.Split(lcaseKey, v.keyDelim)
nested = len(path) > 1
)
// compute the path through the nested maps to the nested value
if nested && v.isPathShadowedInDeepMap(path, castMapStringToMapInterface(v.aliases)) != "" {
return nil
}
// if the requested key is an alias, then return the proper key
lcaseKey = v.realKey(lcaseKey)
path = strings.Split(lcaseKey, v.keyDelim)
nested = len(path) > 1
// 1. 先是判断override有无覆盖的配置 override first
val = v.searchMap(v.override, path)
if val != nil {
return val
}
if nested && v.isPathShadowedInDeepMap(path, v.override) != "" {
return nil
}
// 2. 第二是判断flag中是否存在 PFlag override next
flag, exists := v.pflags[lcaseKey]
if exists && flag.HasChanged() {
switch flag.ValueType() {
case "int", "int8", "int16", "int32", "int64":
return cast.ToInt(flag.ValueString())
case "bool":
return cast.ToBool(flag.ValueString())
case "stringSlice", "stringArray":
s := strings.TrimPrefix(flag.ValueString(), "[")
s = strings.TrimSuffix(s, "]")
res, _ := readAsCSV(s)
return res
case "intSlice":
s := strings.TrimPrefix(flag.ValueString(), "[")
s = strings.TrimSuffix(s, "]")
res, _ := readAsCSV(s)
return cast.ToIntSlice(res)
case "stringToString":
return stringToStringConv(flag.ValueString())
default:
return flag.ValueString()
}
}
if nested && v.isPathShadowedInFlatMap(path, v.pflags) != "" {
return nil
}
// 3. 第三是在Env中查找 Env override next
if v.automaticEnvApplied {
// 判断是不是启动了automaticEnv配置,然后将EnvPrefix设置了,在Env中读取相关的配置
if val, ok := v.getEnv(v.mergeWithEnvPrefix(lcaseKey)); ok {
// 如果env中存在了某项配置,直接返回。
// 到此,可以看出,如果要通过env只覆盖database中的DatabaseName的话,只能将配置文件中的database配置全部写到env中。
// 然后将val(string类型)转换成map型 //TODO 使用 Hook 函数进行操作
return val
}
if nested && v.isPathShadowedInAutoEnv(path) != "" {
return nil
}
}
envkeys, exists := v.env[lcaseKey]
if exists {
for _, envkey := range envkeys {
if val, ok := v.getEnv(envkey); ok {
return val
}
}
}
if nested && v.isPathShadowedInFlatMap(path, v.env) != "" {
return nil
}
// Config file next
val = v.searchIndexableWithPathPrefixes(v.config, path)
if val != nil {
return val
}
if nested && v.isPathShadowedInDeepMap(path, v.config) != "" {
return nil
}
// K/V store next
val = v.searchMap(v.kvstore, path)
if val != nil {
return val
}
if nested && v.isPathShadowedInDeepMap(path, v.kvstore) != "" {
return nil
}
// Default next
val = v.searchMap(v.defaults, path)
if val != nil {
return val
}
if nested && v.isPathShadowedInDeepMap(path, v.defaults) != "" {
return nil
}
if flagDefault {
// last chance: if no value is found and a flag does exist for the key,
// get the flag's default value even if the flag's value has not been set.
if flag, exists := v.pflags[lcaseKey]; exists {
switch flag.ValueType() {
case "int", "int8", "int16", "int32", "int64":
return cast.ToInt(flag.ValueString())
case "bool":
return cast.ToBool(flag.ValueString())
case "stringSlice", "stringArray":
s := strings.TrimPrefix(flag.ValueString(), "[")
s = strings.TrimSuffix(s, "]")
res, _ := readAsCSV(s)
return res
case "intSlice":
s := strings.TrimPrefix(flag.ValueString(), "[")
s = strings.TrimSuffix(s, "]")
res, _ := readAsCSV(s)
return cast.ToIntSlice(res)
case "stringToString":
return stringToStringConv(flag.ValueString())
default:
return flag.ValueString()
}
}
// last item, no need to check shadowing
}
return nil
}
// 2.2 通过 defaultDecoderConfig(rawVal, opts...) 来构造decode函数的第二个参数
// returns default mapsstructure.DecoderConfig with support of time.Duration values & string slices
func defaultDecoderConfig(output interface{}, opts ...DecoderConfigOption) *mapstructure.DecoderConfig {
c := &mapstructure.DecoderConfig{
Metadata: nil,
Result: output,
WeaklyTypedInput: true,
DecodeHook: mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
),
}
for _, opt := range opts {
opt(c)
}
return c
}
// 3. 接下来再调用 decode函数
// input为需要解码的数据,config为解码的方法或配置
func decode(input interface{}, config *mapstructure.DecoderConfig) error {
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
return err
}
return decoder.Decode(input)
}
通过 viper.DecodeHook()解决问题
// 1. 定义Hook函数
func JsonStringToStruct(m interface{}) func(rf reflect.Kind, rt reflect.Kind, data interface{}) (interface{}, error) {
return func(rf reflect.Kind, rt reflect.Kind, data interface{}) (interface{}, error) {
if rf != reflect.String || rt != reflect.Struct {
return data, nil
}
raw := data.(string)
if raw == "" {
return m, nil
}
// 将在env中读取到的json string, 转换为对应的结构体
err := json.Unmarshal([]byte(raw), &m)
return m, err
}
}
// 2. 在上述代码 调用UnmarshalKey函数 的地方,添加进hook函数,如下:
err = setting.vp.UnmarshalKey("groups", &GroupsSetting, viper.DecodeHook(JsonStringToStruct(GroupsSetting)))
// 3. 注意事项:添加到环境变量的时候,需要将groups全部加到环境变量中,[也是该方案的一个缺点]
export APP_TEST_GROUPS="{ \"User\":\"user_zlr\",\"Role\":\"role_zlr\",\"Maner\":\"maner_zlr\"}"
// 一定要将json string格式写对,不然解析会报错
【总结】
上述方案可以通过viper本身的设计,以精简的代码完成需求, 但在env整体覆盖config中的某个模块的全部配置文件,是否更方便?不过在Google里查到的解决方案,都是在env中使用 json string 整体覆盖 config file 中配置的。参考 https://github.com/spf13/viper/issues/641
另外该方式有个缺点,在使用 export 将某个模块的配置加到env之后,如果 加到env中的json string中不能包含全部的配置子项,那么该配置将会为空;
但该方式也有个优点,yaml的嵌套层次可以无限递归下去. 亲测有效;
使用反射技术来完成。如果使用如下方案,那么在初始化viper的时候,无需设置 envpreifx 和 automaticenv
pkg/setting/section.go ///
// ReadSection 读取相关配置文件,并使用环境变量中存在的配置项 覆盖掉 配置文件中的相关配置项
func (s *Setting) ReadSection(k string, v interface{}) error {
err := s.vp.UnmarshalKey(k, v)
if err != nil {
return err
}
fields := reflect.ValueOf(v).Elem()
fieldTypes := fields.Type()
fmt.Println(k)
for i := 0; i < fields.NumField(); i++ {
field := fields.Field(i)
fieldType := fieldTypes.Field(i)
uCaseKey := strings.ToUpper(k)
uCaseName := strings.ToUpper(fieldType.Name)
fmt.Println(" ", fieldType.Name, "-->", field.Interface())
envName := strings.Join([]string{uCaseKey, uCaseName}, "_")
if s.vp.Get(envName) != nil {
// 目前能自动适配Bool, int, uint, string, float32 五种基本类型;
// TODO 暂无法适配time.Duration类型
// decode代码参考viper源码进行了修改
decode(envName, s.vp.Get(envName), field)
}
}
return nil
}
func decode(name string, input interface{}, field reflect.Value) error {
var err error
outputKind := getKind(field)
dataVal := reflect.Indirect(reflect.ValueOf(input))
switch outputKind {
case reflect.Bool:
b, err := strconv.ParseBool(dataVal.String())
if err == nil {
field.SetBool(b)
} else if dataVal.String() == "" {
field.SetBool(false)
} else {
return fmt.Errorf("cannot parse '%s' as bool: %s", name, err)
}
case reflect.String:
field.SetString(dataVal.String())
case reflect.Int:
str := dataVal.String()
if str == "" {
str = "0"
}
i, err := strconv.ParseInt(str, 0, field.Type().Bits())
if err == nil {
field.SetInt(i)
} else {
return fmt.Errorf("cannot parse '%s' as int: %s", name, err)
}
case reflect.Uint:
str := dataVal.String()
if str == "" {
str = "0"
}
i, err := strconv.ParseUint(str, 0, field.Type().Bits())
if err == nil {
field.SetUint(i)
} else {
return fmt.Errorf("cannot parse '%s' as uint: %s", name, err)
}
case reflect.Float32:
str := dataVal.String()
if str == "" {
str = "0"
}
f, err := strconv.ParseFloat(str, field.Type().Bits())
if err == nil {
field.SetFloat(f)
} else {
return fmt.Errorf("cannot parse '%s' as float: %s", name, err)
}
default:
// If we reached this point then we weren't able to decode it
return fmt.Errorf("%s: unsupported type: %s", name, outputKind)
}
return err
}
func getKind(val reflect.Value) reflect.Kind {
kind := val.Kind()
switch {
case kind >= reflect.Int && kind <= reflect.Int64:
return reflect.Int
case kind >= reflect.Uint && kind <= reflect.Uint64:
return reflect.Uint
case kind >= reflect.Float32 && kind <= reflect.Float64:
return reflect.Float32
default:
return kind
}
}
Grafana开源项目可以自行检索,非常棒的一个项目;
// grafana/pkg/setting/setting.go
func (cfg *Cfg) Load(args CommandLineArgs) error {
...
// 加载配置文件
iniFile, err := cfg.loadConfiguration(args)
...
}
func (cfg *Cfg) loadConfiguration(args CommandLineArgs) (*ini.File, error) {
...
// parsedFile 为 在配置文件中加载到的数据
// apply environment overrides
err = applyEnvVariableOverrides(parsedFile)
if err != nil {
return nil, err
}
...
}
// 遍历读取到的ini文件,并使用env中存在的配置覆盖掉ini文件中的相应配置
func applyEnvVariableOverrides(file *ini.File) error {
// appliedEnvOverrides slice,用于记录哪些配置被env覆盖了,并将这些配置输出到log中
appliedEnvOverrides = make([]string, 0)
for _, section := range file.Sections() {
for _, key := range section.Keys() {
// 函数 EnvKey 是根据 ini 中 section 和 key 组装成 envkey
envKey := EnvKey(section.Name(), key.Name())
envValue := os.Getenv(envKey)
// 如果envValue存在的话,使用其覆盖掉之前的数据
if len(envValue) > 0 {
key.SetValue(envValue)
appliedEnvOverrides = append(appliedEnvOverrides, fmt.Sprintf("%s=%s", envKey, RedactedValue(envKey, envValue)))
}
}
}
return nil
}