使用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)
	}
}
相关推荐
YashanDB1 小时前
【YashanDB知识库】XMLAGG方法的兼容
数据库·yashandb·崖山数据库
独行soc1 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍11基于XML的SQL注入(XML-Based SQL Injection)
数据库·安全·web安全·漏洞挖掘·sql注入·hw·xml注入
风间琉璃""2 小时前
bugkctf 渗透测试1超详细版
数据库·web安全·网络安全·渗透测试·内网·安全工具
drebander2 小时前
SQL 实战-巧用 CASE WHEN 实现条件分组与统计
大数据·数据库·sql
IvorySQL2 小时前
IvorySQL 4.0 发布:全面支持 PostgreSQL 17
数据库·postgresql·开源数据库·国产数据库·ivorysql
18号房客2 小时前
高级sql技巧进阶教程
大数据·数据库·数据仓库·sql·mysql·时序数据库·数据库架构
Dawnㅤ2 小时前
使用sql实现将一张表的某些字段数据存到另一种表里
数据库·sql
张声录13 小时前
【ETCD】【实操篇(十二)】分布式系统中的“王者之争”:基于ETCD的Leader选举实战
数据库·etcd
运维&陈同学3 小时前
【模块一】kubernetes容器编排进阶实战之基于velero及minio实现etcd数据备份与恢复
数据库·后端·云原生·容器·kubernetes·etcd·minio·velero
有态度的马甲3 小时前
一种基于etcd实践节点自动故障转移的思路
数据库·etcd