// Package utils 提供配置文件加载工具,支持从INI文件加载配置到结构体 // 支持类型: int, string, bool, float, time.Duration 及其切片类型 // 特性: // - 支持默认值标签 `default` // - 支持INI路径标签 `ini:"section.key"` // - 支持时间单位转换标签 `multiplier` // - 自动处理切片类型(逗号分隔) package main 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:"server.port" default:"8080"` // Timeout time.Duration `ini:"server.timeout" multiplier:"1s"` // Features []string `ini:"server.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 }