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

在实际开发中,我们往往会给一个逻辑设计一套配置文件,用于根据不同环境加载不同配置。

比如生产环境和测试环境数据库的地址不一样,我们就需要在配置文件中设置不同的值。但是配置文件中又有一些相同值的配置项,比如数据库的名称等。难道相同的配置要像下面这样写多次吗?

yaml 复制代码
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)用于填充相同的值,然后在不同环境中填充不同的值。比如下例:

yaml 复制代码
# 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取合集,从而变成一个完整的配置。

实现

具体实现如下:

go 复制代码
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
}

调用例子

go 复制代码
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("%s\nconfig: %v\n", v, conf)
	}
}
相关推荐
jllllyuz31 分钟前
matlab实现蚁群算法解决公交车路径规划问题
服务器·前端·数据库
下雨天u1 小时前
maven dependencyManagement标签作用
java·数据库·maven
背帆1 小时前
go的interface接口底层实现
开发语言·后端·golang
代码配咖啡1 小时前
国产数据库工具突围:SQLynx如何解决Navicat的三大痛点?深度体验报告
数据库
清酒伴风(面试准备中......)1 小时前
小白学编程之——数据库如何性能优化
数据库·oracle·性能优化
The Future is mine2 小时前
SQL Server中delete table和truncate table删除全表数据哪个快?
数据库
瀚高PG实验室2 小时前
HGDB插入超长字段报错指示列名的问题处理
数据库
好吃的肘子2 小时前
MongoDB 高可用复制集架构
数据库·mongodb·架构
兮兮能吃能睡3 小时前
Python之with语句
数据库·python
不穿铠甲的穿山甲3 小时前
MySQL-数据库分布式XA事务
数据库·分布式·mysql