使用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)
	}
}
相关推荐
颜颜yan_8 分钟前
企业级时序数据库选型指南:从传统架构向智能时序数据管理的转型之路
数据库·架构·时序数据库
lichenyang45311 分钟前
管理项目服务器连接数据库
数据库·后端
沙振宇17 分钟前
【数据库】通过‌phpMyAdmin‌管理Mysql数据
数据库·mysql
杨云龙UP1 小时前
CentOS Linux 7 (Core)上部署Oracle 11g、19C RAC详细图文教程
数据库·oracle
ezl1fe1 小时前
RAG 每日一技(十八):手写SQL-RAG太累?LangChain的SQL智能体(Agent)前来救驾!
数据库·人工智能·后端
小咖张1 小时前
spring声明式事务,finally 中return对事务回滚的影响
数据库·java 声明式事务
JSON_L1 小时前
MySQL 加锁与解锁函数
数据库·mysql
白鲸开源2 小时前
收藏!史上最全 Apache SeaTunnel Source 连接器盘点 (2025版),一篇通晓数据集成生态
大数据·数据库·开源
MonKingWD3 小时前
MySQL事务篇-事务概念、并发事务问题、隔离级别
数据库·后端·mysql