使用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)
	}
}
相关推荐
raoxiaoya40 分钟前
golang编译时传递参数或注入变量值到程序中
开发语言·后端·golang
远方16093 小时前
53-Oracle sqlhc多版本实操含(23 ai)
大数据·数据库·sql·oracle·database
ZHOU_WUYI4 小时前
Python Minio库连接和操作Minio数据库
数据库·ragflow
安 当 加 密4 小时前
如何通过密钥管理系统实现数据库、操作系统账号和密码的安全管理
网络·数据库·安全
nbsaas-boot4 小时前
基于存储过程的MySQL自动化DDL同步系统设计
数据库·mysql·自动化
摇一摇小肉包的JAVA笔记4 小时前
Redis如何解决缓存击穿,缓存雪崩,缓存穿透
数据库·redis·缓存
wsdchong之小马过河4 小时前
2025虚幻引擎文件与文件夹命名规律
java·数据库·虚幻
东方-教育技术博主5 小时前
spring boot数据库注解
数据库·spring boot·oracle
是紫焅呢6 小时前
I排序算法.go
开发语言·后端·算法·golang·排序算法·学习方法·visual studio code
飞飞帅傅6 小时前
go语言位运算
开发语言·后端·golang