使用Golang反射技术实现一套有默认值的配置解析库

2023-08-09 18:49:44 浏览数 (1)

在实际开发中,我们往往会给一个逻辑设计一套配置文件,用于根据不同环境加载不同配置。 比如生产环境和测试环境数据库的地址不一样,我们就需要在配置文件中设置不同的值。但是配置文件中又有一些相同值的配置项,比如数据库的名称等。难道相同的配置要像下面这样写多次吗?

代码语言:javascript复制
version: 1
pro:
  write_pool:
    url: pro-write.com
    port: 11
    port: 5432
    user_name: write_pool_user_name
    password: write_pool_password
    dbname: dbname
    max_idle_Conns: 1
    max_open_conns: 1
    conn_max_lifetime_seconds: 3
    user:
      username: un
      password: pwd
  read_pool:
    url: pro-read.com
    port: 5432
    user_name: read_pool_user_name
    password: read_pool_password
    dbname: dbname
    max_idle_Conns: 1
    max_open_conns: 1
    conn_max_lifetime_seconds: 3
    user:
      username: un
      password: pwd
dev:
  write_pool:
    url: dev-write.com
    port: 11
    port: 5432
    user_name: write_pool_user_name
    password: write_pool_password
    dbname: dbname
    max_idle_Conns: 1
    max_open_conns: 1
    conn_max_lifetime_seconds: 3
    user:
      username: un
      password: pwd
  read_pool:
    url: dev-read.com
    port: 5432
    user_name: read_pool_user_name
    password: read_pool_password
    dbname: dbname
    max_idle_Conns: 1
    max_open_conns: 1
    conn_max_lifetime_seconds: 3
    user:
      username: un
      password: pwd

一种简单的办法是:我们设置一个默认项(default)用于填充相同的值,然后在不同环境中填充不同的值。比如下例:

代码语言:javascript复制
# db.yaml
version: 1
pro:
  write_pool:
    url: pro-write.com
    port: 11
  read_pool:
    url: pro-read.com
pre:
  write_pool:
    url: pre-write.com
  read_pool:
    url: pre-read.com
test:
  write_pool:
    url: test-write.com
  read_pool:
    url: test-read.com
dev:
  write_pool:
    url: dev-write.com
  read_pool:
    url: dev-read.com
default:
  write_pool:
    port: 5432
    user_name: write_pool_user_name
    password: write_pool_password
    dbname: dbname
    max_idle_Conns: 1
    max_open_conns: 1
    conn_max_lifetime_seconds: 3
    user:
      username: un
      password: pwd
  read_pool:
    port: 5432
    user_name: read_pool_user_name
    password: read_pool_password
    dbname: dbname
    max_idle_Conns: 1
    max_open_conns: 1
    conn_max_lifetime_seconds: 3
    user:
      username: un
      password: pwd

这样我们在取pro、pre、dev和test环境的配置时,会让它们和default取合集,从而变成一个完整的配置。

实现

具体实现如下:

代码语言:javascript复制
package configparser

import (
	"fmt"
	"os"
	"reflect"

	"gopkg.in/yaml.v3"
)

type Config struct {
	Version string      `yaml:"version"`
	Pro     interface{} `yaml:"pro"`
	Pre     interface{} `yaml:"pre"`
	Dev     interface{} `yaml:"dev"`
	Test    interface{} `yaml:"test"`
	Default interface{} `yaml:"default"`
}

const (
	ErrorEnvNotfound = "env [%s] not found"
	KeyFieldTag      = "yaml"
)

func LoadConfigFromFile(filePath string, env string) (string, error) {
	data, err := os.ReadFile(filePath)
	if err != nil {
		return "", err
	}
	return LoadConfigFromMemory(data, env)
}

func LoadConfigFromMemory(configure []byte, env string) (string, error) {
	var config Config
	err := yaml.Unmarshal(configure, &config)
	if err != nil {
		return "", err
	}

	configReflectType := reflect.TypeOf(config)
	for i := 0; i < configReflectType.NumField(); i   {
		structTag := configReflectType.Field(i).Tag.Get(KeyFieldTag)
		if structTag == env {
			envConfigReflect := reflect.ValueOf(config).Field(i).Interface()
			defauleConfigReflectType := config.Default
			if envConfigReflect == nil && defauleConfigReflectType == nil {
				return "", fmt.Errorf(ErrorEnvNotfound, env)
			}
			if envConfigReflect == nil {
				defaultConf, err := yaml.Marshal(config.Default)
				if err != nil {
					return "", err
				}
				return string(defaultConf), nil
			}
			if defauleConfigReflectType == nil {
				envConf, err := yaml.Marshal(reflect.ValueOf(config).Field(i).Interface())
				if err != nil {
					return "", err
				}
				return string(envConf), nil
			}
			merged := mergeMapStringInterface(reflect.ValueOf(config).Field(i).Interface().(map[string]interface{}), config.Default.(map[string]interface{}))
			mergedConf, err := yaml.Marshal(merged)
			if err != nil {
				return "", err
			}
			return string(mergedConf), nil
		}
	}

	return "", fmt.Errorf(ErrorEnvNotfound, env)
}

func mergeMapStringInterface(cover map[string]interface{}, base map[string]interface{}) map[string]interface{} {
	for k, v := range cover {
		switch v.(type) {
		case map[string]interface{}:
			if base[k] == nil {
				base[k] = v
			} else {
				mergeMapStringInterface(v.(map[string]interface{}), base[k].(map[string]interface{}))
			}
		default:
			base[k] = v
		}
	}
	return base
}

调用例子

代码语言:javascript复制
package main

import (
	"fmt"
	configparser "gconf"
	"os"
	"path"
	"gopkg.in/yaml.v3"
)

type PostgresSqlConnConfigs struct {
	WritePool PostgresSqlConnPoolConf `yaml:"write_pool"`
	ReadPool  PostgresSqlConnPoolConf `yaml:"read_pool"`
}

type PostgresSqlConnPoolConf struct {
	Url                    *string `yaml:"url"`
	Port                   *string `yaml:"port"`
	UserName               *string `yaml:"user_name"`
	Password               *string `yaml:"password"`
	DbName                 *string `yaml:"dbname"`
	MaxIdleConn            *int    `yaml:"max_idle_Conns"`
	MaxOpenConn            *int    `yaml:"max_open_conns"`
	ConnMaxLifetimeSeconds *int64  `yaml:"conn_max_lifetime_seconds"`
	UserA                  *User   `yaml:"user"`
}

type User struct {
	Username *string `yaml:"username"`
	Password *string `yaml:"password"`
}

func main() {
	runPath, _ := os.Getwd()
	confPath := path.Join(runPath, "conf/db.yaml")

	env := []string{"dev", "test", "pre", "pro"}
	for _, v := range env {
		var conf ExampleConfig
		curConfig, err := configparser.LoadConfigFromFile(confPath, v)
		if err != nil {
			fmt.Printf("load config file failed, err: %v", err)
		}
		err = yaml.Unmarshal([]byte(curConfig), &conf)
		if err != nil {
			fmt.Printf("unmarshal config file failed, err: %v", err)
		}
		fmt.Printf("%snconfig: %vn", v, conf)
	}
}

0 人点赞