379 lines
11 KiB
Go
379 lines
11 KiB
Go
// Package utils 提供配置文件加载工具,支持从INI文件加载配置到结构体
|
||
// 支持类型: int, string, bool, float, time.Duration 及其切片类型
|
||
// 特性:
|
||
// - 支持默认值标签 `default`
|
||
// - 支持INI路径标签 `ini:"section.key"`
|
||
// - 支持时间单位转换标签 `multiplier`
|
||
// - 自动处理切片类型(逗号分隔)
|
||
package iniConfigUtil
|
||
|
||
import (
|
||
"log"
|
||
"os"
|
||
"reflect"
|
||
"regexp"
|
||
"strconv"
|
||
"time"
|
||
|
||
"github.com/go-ini/ini"
|
||
)
|
||
|
||
// LoadConfig 从INI文件加载配置到结构体
|
||
// 参数:
|
||
//
|
||
// configPtr: 指向配置结构体的指针(必须是结构体指针)
|
||
// filename: INI配置文件的路径//
|
||
//
|
||
// 返回:
|
||
//
|
||
// error: 加载成功返回nil,失败返回ConfigError详细错误信息
|
||
//
|
||
// 用法说明:
|
||
// 1. 定义配置结构体,使用标签声明INI映射关系和默认值
|
||
// 2. 调用LoadConfig(&config, "config.ini")
|
||
// 3. 检查错误并应用配置
|
||
//
|
||
// 示例结构体:
|
||
//
|
||
// type AppConfig struct {
|
||
// Port int `ini:"service.port" default:"8080"`
|
||
// Timeout time.Duration `ini:"service.timeout" multiplier:"1s"`
|
||
// Features []string `ini:"service.features"`
|
||
// }
|
||
func LoadConfig(configPtr interface{}, filename string) error {
|
||
// 验证输入必须是指针
|
||
if reflect.TypeOf(configPtr).Kind() != reflect.Ptr {
|
||
return &ConfigError{Message: "configPtr必须是指向结构体的指针"} // 返回错误
|
||
}
|
||
|
||
// 验证输入必须指向结构体
|
||
configValue := reflect.ValueOf(configPtr).Elem()
|
||
// 确保输入是结构体
|
||
if configValue.Kind() != reflect.Struct {
|
||
return &ConfigError{Message: "configPtr必须指向结构体"} // 返回错误
|
||
}
|
||
|
||
// 设置默认值(如果结构体有默认值)
|
||
setDefaultValues(configValue)
|
||
|
||
// 检查配置文件是否存在
|
||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||
log.Printf("配置文件不存在: %s, 使用默认值", filename) // 打印信息
|
||
return nil // 返回错误
|
||
}
|
||
|
||
// 加载INI文件
|
||
iniCfg, err := ini.Load(filename)
|
||
// 处理错误
|
||
if err != nil {
|
||
return &ConfigError{Message: "加载配置文件失败", Cause: err} // 返回错误
|
||
}
|
||
|
||
// 映射配置到结构体
|
||
if err := mapConfig(iniCfg, configValue); err != nil {
|
||
return err // 返回错误
|
||
}
|
||
|
||
// 处理特殊类型(如time.Duration)
|
||
processSpecialTypes(configValue)
|
||
|
||
// 返回成功
|
||
return nil
|
||
}
|
||
|
||
// setDefaultValues 设置结构体字段的默认值
|
||
// 遍历结构体字段,检测`default`标签并设置初始值
|
||
// setDefaultValues 设置结构体字段的默认值
|
||
func setDefaultValues(configValue reflect.Value) {
|
||
// 获取结构体类型
|
||
configType := configValue.Type()
|
||
|
||
// 遍历字段
|
||
for i := 0; i < configType.NumField(); i++ {
|
||
// 获取字段信息
|
||
field := configType.Field(i)
|
||
// 获取字段值
|
||
fieldValue := configValue.Field(i)
|
||
|
||
// 如果字段是结构体,递归处理
|
||
if fieldValue.Kind() == reflect.Struct {
|
||
setDefaultValues(fieldValue)
|
||
continue
|
||
}
|
||
|
||
// 如果字段已经设置了值,跳过
|
||
if !fieldValue.IsZero() {
|
||
continue // 跳过
|
||
}
|
||
|
||
// 检查是否有默认值标签
|
||
if defaultValue, ok := field.Tag.Lookup("default"); ok {
|
||
setValueFromString(fieldValue, defaultValue) // 设置字段值
|
||
}
|
||
}
|
||
}
|
||
|
||
// mapConfig 将INI配置映射到结构体字段
|
||
// 解析`ini`标签获取section和key,读取对应配置值
|
||
func mapConfig(iniCfg *ini.File, configValue reflect.Value) error {
|
||
// 获取结构体类型
|
||
configType := configValue.Type()
|
||
|
||
// 遍历字段
|
||
for i := 0; i < configType.NumField(); i++ {
|
||
field := configType.Field(i) // 获取字段信息
|
||
fieldValue := configValue.Field(i) // 获取字段值
|
||
|
||
// 如果字段是结构体,递归处理
|
||
if fieldValue.Kind() == reflect.Struct {
|
||
if err := mapConfig(iniCfg, fieldValue); err != nil {
|
||
return err
|
||
}
|
||
continue
|
||
}
|
||
|
||
// 获取INI标签
|
||
iniTag, ok := field.Tag.Lookup("ini")
|
||
// 如果没有INI标签,跳过这个字段
|
||
if !ok || iniTag == "" {
|
||
continue // 跳过
|
||
}
|
||
|
||
// 解析INI键名(支持section.key格式)
|
||
sectionName, keyName := parseIniTag(iniTag)
|
||
|
||
// 获取INI值
|
||
section, err := iniCfg.GetSection(sectionName)
|
||
// 如果section不存在
|
||
if err != nil {
|
||
continue // 跳过这个字段
|
||
}
|
||
|
||
// 获取INI键值
|
||
key, err := section.GetKey(keyName)
|
||
// 如果key不存在
|
||
if err != nil {
|
||
continue //跳过这个字段
|
||
}
|
||
|
||
// 设置结构体字段值
|
||
if err := setFieldValue(fieldValue, key); err != nil {
|
||
// 返回错误
|
||
return &ConfigError{
|
||
Message: "设置字段值失败", // 返回错误信息
|
||
Cause: err, // 返回错误原因
|
||
Field: field.Name, // 返回字段名称
|
||
Tag: iniTag, // 返回标签
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// parseIniTag 解析INI标签格式
|
||
// 输入: "section.key" 格式的字符串
|
||
// 返回: (section名称, key名称)
|
||
func parseIniTag(tag string) (section, key string) {
|
||
// 默认section
|
||
section = "DEFAULT"
|
||
// 默认key
|
||
key = tag
|
||
|
||
// 检查是否有section前缀
|
||
if parts := regexp.MustCompile(`^(\w+)\.(\w+)$`).FindStringSubmatch(tag); len(parts) == 3 {
|
||
section = parts[1] // 设置section
|
||
key = parts[2] // 设置key
|
||
}
|
||
|
||
// 返回section和key
|
||
return section, key
|
||
}
|
||
|
||
// setFieldValue 根据INI键值设置结构体字段值
|
||
// 自动处理基础类型和time.Duration类型转换
|
||
func setFieldValue(fieldValue reflect.Value, key *ini.Key) error {
|
||
// 检查字段类型
|
||
switch fieldValue.Kind() {
|
||
case reflect.String: // 字符串类型
|
||
fieldValue.SetString(key.String()) // 设置字段值
|
||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: // 整数类型
|
||
// 特殊处理time.Duration类型
|
||
if fieldValue.Type() == reflect.TypeOf(time.Duration(0)) {
|
||
duration, err := time.ParseDuration(key.String()) // 解析字符串为time.Duration
|
||
// 处理错误
|
||
if err != nil {
|
||
return err // 返回错误
|
||
}
|
||
fieldValue.SetInt(int64(duration)) // 设置字段值
|
||
} else {
|
||
intValue, err := key.Int() // 获取整数值
|
||
// 处理错误
|
||
if err != nil {
|
||
return err // 返回错误
|
||
}
|
||
fieldValue.SetInt(int64(intValue)) // 设置字段值
|
||
}
|
||
case reflect.Bool: // 布尔类型
|
||
boolValue, err := key.Bool() // 获取布尔值
|
||
// 处理错误
|
||
if err != nil {
|
||
return err // 返回错误
|
||
}
|
||
fieldValue.SetBool(boolValue) // 设置字段值
|
||
case reflect.Float32, reflect.Float64: // 浮点类型
|
||
floatValue, err := key.Float64() // 获取浮点值
|
||
// 处理错误
|
||
if err != nil {
|
||
return err // 返回错误
|
||
}
|
||
fieldValue.SetFloat(floatValue) // 设置字段值
|
||
case reflect.Slice: // 切片类型
|
||
return setSliceValue(fieldValue, key) // 处理切片类型字段
|
||
default: // 其他类型
|
||
return &ConfigError{Message: "不支持的字段类型", FieldType: fieldValue.Type().String()} // 返回错误
|
||
}
|
||
|
||
// 返回成功
|
||
return nil
|
||
}
|
||
|
||
// setSliceValue 设置切片类型字段值
|
||
// 将逗号分隔的字符串解析为指定类型的切片
|
||
func setSliceValue(fieldValue reflect.Value, key *ini.Key) error {
|
||
// 获取切片元素类型
|
||
sliceType := fieldValue.Type().Elem()
|
||
// 获取逗号分隔的值
|
||
values := key.Strings(",")
|
||
|
||
// 创建新切片
|
||
slice := reflect.MakeSlice(fieldValue.Type(), len(values), len(values))
|
||
|
||
// 遍历值
|
||
for i, val := range values {
|
||
elemValue := reflect.New(sliceType).Elem() // 创建新元素
|
||
|
||
// 根据切片元素类型设置值
|
||
switch sliceType.Kind() {
|
||
case reflect.String: // 字符串类型
|
||
elemValue.SetString(val) // 设置字段值
|
||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: // 整数类型
|
||
intVal, err := strconv.ParseInt(val, 10, 64) // 解析字符串为整数
|
||
// 处理错误
|
||
if err != nil {
|
||
return err // 返回错误
|
||
}
|
||
elemValue.SetInt(intVal) // 设置字段值
|
||
case reflect.Float32, reflect.Float64: // 浮点类型
|
||
floatVal, err := strconv.ParseFloat(val, 64) // 解析字符串为浮点数
|
||
// 处理错误
|
||
if err != nil {
|
||
return err // 返回错误
|
||
}
|
||
elemValue.SetFloat(floatVal) // 设置字段值
|
||
case reflect.Bool: // 布尔类型
|
||
boolVal, err := strconv.ParseBool(val) // 解析字符串为布尔值
|
||
// 处理错误
|
||
if err != nil {
|
||
return err // 返回错误
|
||
}
|
||
elemValue.SetBool(boolVal) // 设置字段值
|
||
default:
|
||
return &ConfigError{Message: "不支持的切片元素类型", FieldType: sliceType.String()} // 返回错误
|
||
}
|
||
|
||
slice.Index(i).Set(elemValue) // 设置切片元素
|
||
}
|
||
|
||
// 设置切片字段值
|
||
fieldValue.Set(slice)
|
||
// 返回成功
|
||
return nil
|
||
}
|
||
|
||
// setValueFromString 从字符串解析值到结构体字段(用于默认值)
|
||
// 支持基础类型转换,不处理复杂类型
|
||
func setValueFromString(fieldValue reflect.Value, valueStr string) {
|
||
switch fieldValue.Kind() {
|
||
case reflect.String:
|
||
fieldValue.SetString(valueStr)
|
||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||
if intValue, err := strconv.ParseInt(valueStr, 10, 64); err == nil {
|
||
fieldValue.SetInt(intValue)
|
||
}
|
||
case reflect.Bool:
|
||
if boolValue, err := strconv.ParseBool(valueStr); err == nil {
|
||
fieldValue.SetBool(boolValue)
|
||
}
|
||
case reflect.Float32, reflect.Float64:
|
||
if floatValue, err := strconv.ParseFloat(valueStr, 64); err == nil {
|
||
fieldValue.SetFloat(floatValue)
|
||
}
|
||
case reflect.Slice:
|
||
// 切片类型需要特殊处理,这里简化处理
|
||
default:
|
||
// 保留原panic调用,但修改提示信息
|
||
panic("未处理的默认值类型")
|
||
}
|
||
}
|
||
|
||
// processSpecialTypes 处理特殊类型转换
|
||
// 当前支持time.Duration的倍数转换(使用multiplier标签)
|
||
// processSpecialTypes 处理特殊类型转换
|
||
func processSpecialTypes(configValue reflect.Value) {
|
||
// 获取结构体类型
|
||
configType := configValue.Type()
|
||
|
||
// 遍历结构体字段
|
||
for i := 0; i < configType.NumField(); i++ {
|
||
field := configType.Field(i) // 获取字段
|
||
fieldValue := configValue.Field(i) // 获取字段值
|
||
|
||
// 如果字段是结构体,递归处理
|
||
if fieldValue.Kind() == reflect.Struct {
|
||
processSpecialTypes(fieldValue)
|
||
continue
|
||
}
|
||
|
||
// 处理time.Duration类型的倍数转换
|
||
if fieldValue.Type() == reflect.TypeOf(time.Duration(0)) {
|
||
// 检查是否存在multiplier标签
|
||
if multiplier, ok := field.Tag.Lookup("multiplier"); ok {
|
||
// 解析multiplier标签
|
||
if mult, err := time.ParseDuration(multiplier); err == nil {
|
||
duration := time.Duration(fieldValue.Int()) // 将字段值转换为time.Duration
|
||
fieldValue.SetInt(int64(duration * mult)) // 将字段值设置为转换后的时间
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ConfigError 自定义配置错误类型
|
||
// 包含错误原因、字段信息和原始错误
|
||
type ConfigError struct {
|
||
Message string
|
||
Cause error
|
||
Field string
|
||
FieldType string
|
||
Tag string
|
||
}
|
||
|
||
// Error 实现error接口,提供详细错误信息
|
||
func (e *ConfigError) Error() string {
|
||
msg := "配置错误: " + e.Message
|
||
if e.Field != "" {
|
||
msg += " [字段: " + e.Field + "]"
|
||
}
|
||
if e.FieldType != "" {
|
||
msg += " [类型: " + e.FieldType + "]"
|
||
}
|
||
if e.Tag != "" {
|
||
msg += " [标签: " + e.Tag + "]"
|
||
}
|
||
if e.Cause != nil {
|
||
msg += " - " + e.Cause.Error()
|
||
}
|
||
return msg
|
||
}
|