diff --git a/README.md b/README.md index 680acce..b18a31d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@ Reflux 是一个 Go 语言包,提供了统一的接口用于访问和操作嵌 - 🔄 **类型转换**: 基于 [spf13/cast](https://github.com/spf13/cast) 的强大类型转换支持 - ✏️ **修改数据**: 支持通过路径设置和删除值 - 🔗 **链式调用**: Set 和 Delete 方法支持链式调用,如 `rfx.Set("name", "Alice").Delete("age").Set("city", "Beijing")` -- 📋 **深度克隆**: Scope 方法创建深度克隆,修改不影响原始数据 +- 📋 **深度克隆**: Scope 方法和值传递模式创建深度克隆,修改不影响原始数据 +- 🎭 **灵活模式**: 支持指针和值两种传递方式,提供不同的数据操作语义 +- 🔌 **Interface 支持**: 自动解析 `interface{}` 类型到实际类型 - 🔤 **大小写不敏感**: 支持使用小写字段名访问和修改结构体字段 - 🔑 **键名提取**: Keys 方法可获取结构体或 Map 的所有键名 - 📝 **JSON 集成**: Ptr() 方法返回的指针可直接用于 json.Unmarshal(),实现动态 JSON 反序列化 @@ -68,12 +70,62 @@ func main() { ### 1. 创建 Reflux +Reflux 支持**指针**和**值**两种传递方式,提供不同的数据操作语义: + +#### 指针模式 - 直接修改原始数据 + ```go user := User{Name: "Alice"} -rfx := reflux.New(&user) // 传入指针以支持修改 +rfx := reflux.New(&user) // 传入指针 + +rfx.Set("Name", "Bob") +// user.Name 已被修改为 "Bob" ``` -**重要**: `New()` 函数必须传入指针类型,否则会 panic。 +#### 值模式 - 深度克隆,不影响原始数据 + +```go +user := User{Name: "Alice"} +rfx := reflux.New(user) // 传入值 + +rfx.Set("Name", "Bob") +// user.Name 仍然是 "Alice" +// rfx 内部是独立的深度克隆 +``` + +#### Interface 类型支持 + +Reflux 支持 `interface{}` (或 `any`) 类型,会自动解析到实际类型: + +```go +// interface{} 包装的值 - 创建深度克隆 +var config any = map[string]string{"host": "localhost"} +rfx := reflux.New(config) +rfx.Set("host", "127.0.0.1") +// 原始 config 不受影响 + +// interface{} 包装的指针 - 直接修改 +var data any = &config +rfx := reflux.New(data) +rfx.Set("host", "127.0.0.1") +// config 已被修改 +``` + +#### 支持的类型 + +| 类型 | 说明 | 示例 | +|-----|------|------| +| **Struct** | 结构体 | `New(Person{})` 或 `New(&person)` | +| **Map** | 映射 | `New(map[string]any{})` 或 `New(&m)` | +| **Slice** | 切片 | `New([]string{})` 或 `New(&s)` | +| **Array** | 数组 | `New([3]int{})` 或 `New(&a)` | +| **Interface** | 接口 | `New(anyValue)` - 自动解析 | + +**重要说明**: +- ✅ **指针模式**: 传入指针(如 `&user`)时,所有修改都会**直接影响原始数据** +- ✅ **值模式**: 传入值(如 `user`)时,会创建**深度克隆**,修改不影响原始数据 +- ✅ **深度克隆**: 对于 map 和 slice 等引用类型,值模式也会创建完全独立的副本 +- ❌ **不支持**: 基本类型(int, string, bool)、chan、func 等不是容器的类型 ### 2. 获取值 (Get) @@ -581,6 +633,48 @@ func main() { - **插件热加载**: 更新插件配置而无需重启 - **API 部分响应**: 只处理 API 返回的部分字段 +### 7. 泛型数据处理 (Interface 类型) + +使用 interface{} 类型处理未知类型的数据: + +```go +// 函数返回 any 类型 +func loadFromAPI() any { + // 可能返回 map 或 struct + return map[string]any{ + "user": map[string]any{ + "name": "Alice", + "age": 30, + }, + } +} + +func processData(data any) { + // 自动解析 interface{} 到实际类型 + rfx := reflux.New(data) // 值模式,创建深度克隆 + + // 安全地修改数据 + rfx.Set("user.name", "Bob") + rfx.Set("user.age", 35) + + // 原始数据不受影响 +} + +// 或者使用指针模式 +func updateData(dataPtr any) { + // interface{} 包装指针,直接修改 + rfx := reflux.New(dataPtr) + rfx.Set("user.name", "Charlie") + // 原始数据已被修改 +} +``` + +使用场景: +- **插件系统**: 处理未知结构的插件配置 +- **泛型配置**: 统一处理不同格式的配置数据 +- **API 适配**: 适配多种 API 响应格式 +- **数据转换**: 在不同数据结构间转换 + ## 路径语法 Reflux 支持灵活的点号路径语法: @@ -667,16 +761,20 @@ type Reflux interface { ## 注意事项 -1. **指针传递**: `New()` 函数必须传递指针,否则会 panic -2. **Scope 行为**: `Scope` 返回深度克隆,修改不会影响原始数据 -3. **链式调用**: Set 和 Delete 方法都支持链式调用,返回 Reflux 自身 -4. **类型转换**: 使用 spf13/cast 进行类型转换,转换失败会 panic -5. **切片删除**: 删除切片元素会创建新切片并重新赋值 -6. **Map 初始化**: 对 nil map 调用 Set 会自动初始化 map -7. **大小写不敏感**: 支持使用小写字段名访问结构体字段 -8. **JSON 集成**: Ptr() 返回的指针可以安全地用于 json.Unmarshal() -9. **并发安全**: Reflux 本身不是并发安全的,需要外部同步 -10. **错误处理**: 大多数转换、设置和删除操作失败时会 panic,请确保路径和类型正确 +1. **指针 vs 值传递**: + - 传入**指针** (`New(&data)`) 时,修改会影响原始数据 + - 传入**值** (`New(data)`) 时,会创建深度克隆,修改不影响原始数据 + - 对于 map 和 slice 等引用类型,值模式也会完全克隆 +2. **Interface 支持**: 支持 `interface{}` 类型,会自动解析到实际类型并保持正确的指针/值语义 +3. **Scope 行为**: `Scope` 返回深度克隆,修改不会影响原始数据 +4. **链式调用**: Set 和 Delete 方法都支持链式调用,返回 Reflux 自身 +5. **类型转换**: 使用 spf13/cast 进行类型转换,转换失败会 panic +6. **切片删除**: 删除切片元素会创建新切片并重新赋值 +7. **Map 初始化**: 对 nil map 调用 Set 会自动初始化 map +8. **大小写不敏感**: 支持使用小写字段名访问结构体字段 +9. **JSON 集成**: Ptr() 返回的指针可以安全地用于 json.Unmarshal() +10. **并发安全**: Reflux 本身不是并发安全的,需要外部同步 +11. **错误处理**: 大多数转换、设置和删除操作失败时会 panic,请确保路径和类型正确 ## 示例代码 diff --git a/reflux.go b/reflux.go index 918a26f..13bd6d1 100644 --- a/reflux.go +++ b/reflux.go @@ -119,13 +119,49 @@ type R interface { } // New 创建一个新的 R 实例 -// 参数 v 必须是指针类型,否则会 panic +// 参数 v 可以是指针或非指针类型 +// - 如果传入指针: 将直接使用该指针,可以修改原始数据 +// - 如果传入值: 会自动创建一个深度克隆的指针副本,修改不影响原始数据 +// 支持的类型: map、struct、slice、array +// 也支持 interface 类型,会自动解析到实际类型 // 返回一个 R 接口实例,可用于访问和操作嵌套的字段、元素和键值对 func New(v any) R { rv := reflect.ValueOf(v) - // 如果传入的不是指针,panic - if rv.Kind() != reflect.Ptr { - panic("structmap: New() requires a pointer argument") + + if !rv.IsValid() { + panic("rfx: invalid value") } + + // 记录原始是否为指针 + isPtr := rv.Kind() == reflect.Ptr + + // 递归解引用指针和接口,直到获取实际的值类型 + actualValue := rv + for actualValue.Kind() == reflect.Ptr || actualValue.Kind() == reflect.Interface { + if actualValue.IsNil() { + panic("rfx: cannot use nil pointer or nil interface") + } + actualValue = actualValue.Elem() + // 如果解引用后的类型是指针,标记为指针模式 + if actualValue.Kind() == reflect.Ptr { + isPtr = true + } + } + + // 检查最终的实际类型是否为支持的类型 + switch actualValue.Kind() { + case reflect.Map, reflect.Struct, reflect.Slice, reflect.Array: + // 支持的类型 + default: + panic("rfx: unsupported type, only map, struct, slice, and array are allowed") + } + + // 如果原始传入的不是指针类型,需要进行深度克隆以避免修改原始数据 + // 对于引用类型(map, slice)这尤其重要 + if !isPtr { + // 使用深度克隆创建一个完全独立的副本 + rv = DeepClone(actualValue) + } + return &rfx{value: rv} } diff --git a/rfx_example_test.go b/rfx_example_test.go index a0ac3e7..04275b5 100644 --- a/rfx_example_test.go +++ b/rfx_example_test.go @@ -2,6 +2,228 @@ package reflux import "fmt" +// ExampleNew_withPointer 演示如何使用 New 函数传入指针 +// 传入指针时,修改会影响原始数据 +func ExampleNew_withPointer() { + type Person struct { + Name string + Age int + } + + person := Person{Name: "Alice", Age: 30} + + // 传入指针 - 修改会影响原始数据 + rfx := New(&person) + rfx.Set("Name", "Bob") + rfx.Set("Age", 35) + + // 原始数据已被修改 + fmt.Printf("Name: %s, Age: %d\n", person.Name, person.Age) + + // Output: + // Name: Bob, Age: 35 +} + +// ExampleNew_withPointer_map 演示传入 map 指针 +func ExampleNew_withPointer_map() { + config := map[string]any{ + "host": "localhost", + "port": 8080, + } + + // 传入 map 指针 - 修改会影响原始数据 + rfx := New(&config) + rfx.Set("host", "127.0.0.1") + rfx.Set("port", 9090) + + // 原始 map 已被修改 + fmt.Printf("Host: %s, Port: %v\n", config["host"], config["port"]) + + // Output: + // Host: 127.0.0.1, Port: 9090 +} + +// ExampleNew_withPointer_slice 演示传入 slice 指针 +func ExampleNew_withPointer_slice() { + items := []string{"apple", "banana", "cherry"} + + // 传入 slice 指针 - 修改会影响原始数据 + rfx := New(&items) + rfx.Set("0", "orange") + rfx.Set("2", "grape") + + // 原始 slice 已被修改 + fmt.Printf("Items: %v\n", items) + + // Output: + // Items: [orange banana grape] +} + +// ExampleNew_withValue 演示如何使用 New 函数传入值 +// 传入值时,会创建深度克隆,修改不会影响原始数据 +func ExampleNew_withValue() { + type Person struct { + Name string + Age int + } + + person := Person{Name: "Alice", Age: 30} + + // 传入值 - 会创建深度克隆,修改不会影响原始数据 + rfx := New(person) + rfx.Set("Name", "Bob") + rfx.Set("Age", 35) + + // 原始数据未被修改 + fmt.Printf("Original - Name: %s, Age: %d\n", person.Name, person.Age) + + // rfx 内部的数据已修改 + fmt.Printf("Clone - Name: %s, Age: %d\n", + rfx.Get("Name").String(), + rfx.Get("Age").Int()) + + // Output: + // Original - Name: Alice, Age: 30 + // Clone - Name: Bob, Age: 35 +} + +// ExampleNew_withValue_map 演示传入 map 值 +// 传入值时会创建深度克隆,即使 map 是引用类型 +func ExampleNew_withValue_map() { + config := map[string]any{ + "host": "localhost", + "port": 8080, + } + + // 传入 map 值 - 会创建深度克隆 + rfx := New(config) + rfx.Set("host", "127.0.0.1") + rfx.Set("port", 9090) + + // 原始 map 未被修改 + fmt.Printf("Original - Host: %s, Port: %v\n", config["host"], config["port"]) + + // rfx 内部的数据已修改 + fmt.Printf("Clone - Host: %s, Port: %v\n", + rfx.Get("host").String(), + rfx.Get("port").Int()) + + // Output: + // Original - Host: localhost, Port: 8080 + // Clone - Host: 127.0.0.1, Port: 9090 +} + +// ExampleNew_withValue_slice 演示传入 slice 值 +// 传入值时会创建深度克隆,即使 slice 是引用类型 +func ExampleNew_withValue_slice() { + items := []string{"apple", "banana", "cherry"} + + // 传入 slice 值 - 会创建深度克隆 + rfx := New(items) + rfx.Set("0", "orange") + rfx.Set("2", "grape") + + // 原始 slice 未被修改 + fmt.Printf("Original: %v\n", items) + + // rfx 内部的数据已修改 + fmt.Printf("Clone: [%s %s %s]\n", + rfx.Get("0").String(), + rfx.Get("1").String(), + rfx.Get("2").String()) + + // Output: + // Original: [apple banana cherry] + // Clone: [orange banana grape] +} + +// ExampleNew_withValue_nestedStruct 演示传入嵌套结构体值 +func ExampleNew_withValue_nestedStruct() { + type Address struct { + City string + Street string + } + type Person struct { + Name string + Address Address + } + + person := Person{ + Name: "Alice", + Address: Address{ + City: "Beijing", + Street: "Main St", + }, + } + + // 传入值 - 会创建深度克隆,包括嵌套的结构体 + rfx := New(person) + rfx.Set("Name", "Bob") + rfx.Set("Address.City", "Shanghai") + + // 原始数据未被修改 + fmt.Printf("Original - Name: %s, City: %s\n", person.Name, person.Address.City) + + // rfx 内部的数据已修改 + fmt.Printf("Clone - Name: %s, City: %s\n", + rfx.Get("Name").String(), + rfx.Get("Address", "City").String()) + + // Output: + // Original - Name: Alice, City: Beijing + // Clone - Name: Bob, City: Shanghai +} + +// ExampleNew_withInterface 演示 interface{} 类型的支持 +// 当数据被包装在 interface{} 中时,会自动解析到实际类型 +func ExampleNew_withInterface() { + // interface{} 包装的 map + var config any = map[string]any{ + "host": "localhost", + "port": 8080, + } + + // 传入 interface{} 值 - 会创建深度克隆 + rfx := New(config) + rfx.Set("host", "127.0.0.1") + rfx.Set("port", 9090) + + // 原始数据未被修改 + originalMap := config.(map[string]any) + fmt.Printf("Original - Host: %s, Port: %v\n", originalMap["host"], originalMap["port"]) + + // rfx 内部的数据已修改 + fmt.Printf("Clone - Host: %s, Port: %v\n", + rfx.Get("host").String(), + rfx.Get("port").Int()) + + // Output: + // Original - Host: localhost, Port: 8080 + // Clone - Host: 127.0.0.1, Port: 9090 +} + +// ExampleNew_withInterfacePointer 演示 interface{} 包装指针的情况 +func ExampleNew_withInterfacePointer() { + type Config struct { + Host string + Port int + } + + config := Config{Host: "localhost", Port: 8080} + + // interface{} 包装指针 - 修改会影响原始数据 + var data any = &config + rfx := New(data) + rfx.Set("Host", "127.0.0.1") + rfx.Set("Port", 9090) + + // 原始数据已被修改(因为是指针) + fmt.Printf("Host: %s, Port: %d\n", config.Host, config.Port) + + // Output: + // Host: 127.0.0.1, Port: 9090 +} + // ExampleNew_withMap 演示如何使用 New 函数传入 map func ExampleNew_withMap() { // 创建一个 map diff --git a/rfx_test.go b/rfx_test.go index 0fd184f..11b541d 100644 --- a/rfx_test.go +++ b/rfx_test.go @@ -23,15 +23,155 @@ type Address struct { // TestNew 测试 New 函数 func TestNew(t *testing.T) { - p := Person{Name: "Alice", Age: 30} + t.Run("New with pointer", func(t *testing.T) { + // 测试传入指针 - 修改应该影响原始数据 + p := Person{Name: "Alice", Age: 30} + rfx := New(&p) - // 测试传入非指针值应该 panic - defer func() { - if r := recover(); r == nil { - t.Error("New() with non-pointer should panic") + rfx.Set("Name", "Bob") + if p.Name != "Bob" { + t.Errorf("Expected original value to be modified, got '%s'", p.Name) } - }() - New(p) // 应该 panic + }) + + t.Run("New with value", func(t *testing.T) { + // 测试传入值 - 修改不应该影响原始数据 + p := Person{Name: "Alice", Age: 30} + rfx := New(p) + + rfx.Set("Name", "Bob") + // 原始值不应该被修改 + if p.Name != "Alice" { + t.Errorf("Expected original value to remain 'Alice', got '%s'", p.Name) + } + + // rfx 内部的值应该已经修改 + if rfx.Get("Name").String() != "Bob" { + t.Errorf("Expected rfx value to be 'Bob', got '%s'", rfx.Get("Name").String()) + } + }) + + t.Run("New with map pointer", func(t *testing.T) { + m := map[string]any{"name": "Alice"} + rfx := New(&m) + + rfx.Set("name", "Bob") + if m["name"] != "Bob" { + t.Errorf("Expected map to be modified, got '%v'", m["name"]) + } + }) + + t.Run("New with map value", func(t *testing.T) { + m := map[string]any{"name": "Alice"} + rfx := New(m) + + rfx.Set("name", "Bob") + // 原始 map 不应该被修改 + if m["name"] != "Alice" { + t.Errorf("Expected original map to remain 'Alice', got '%v'", m["name"]) + } + }) + + t.Run("New with slice pointer", func(t *testing.T) { + s := []string{"a", "b", "c"} + rfx := New(&s) + + rfx.Set("0", "x") + if s[0] != "x" { + t.Errorf("Expected slice to be modified, got '%s'", s[0]) + } + }) + + t.Run("New with slice value", func(t *testing.T) { + s := []string{"a", "b", "c"} + rfx := New(s) + + rfx.Set("0", "x") + // 原始切片不应该被修改 + if s[0] != "a" { + t.Errorf("Expected original slice to remain 'a', got '%s'", s[0]) + } + }) + + t.Run("New with array value", func(t *testing.T) { + a := [3]string{"a", "b", "c"} + rfx := New(a) + + rfx.Set("0", "x") + // 原始数组不应该被修改 + if a[0] != "a" { + t.Errorf("Expected original array to remain 'a', got '%s'", a[0]) + } + }) + + t.Run("New with nil pointer", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic with nil pointer") + } + }() + var p *Person + New(p) + }) + + t.Run("New with unsupported type", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic with unsupported type") + } + }() + New(42) // int 不支持 + }) + + t.Run("New with interface wrapping map", func(t *testing.T) { + // 测试 interface{} 包装的 map + var data any = map[string]string{"name": "Alice"} + rfx := New(data) + + if rfx.Get("name").String() != "Alice" { + t.Errorf("Expected 'Alice', got '%s'", rfx.Get("name").String()) + } + + // 修改不应该影响原始数据(因为是值传递) + rfx.Set("name", "Bob") + originalMap := data.(map[string]string) + if originalMap["name"] != "Alice" { + t.Error("Original map should not be modified") + } + }) + + t.Run("New with interface wrapping struct", func(t *testing.T) { + // 测试 interface{} 包装的 struct + var data any = Person{Name: "Alice", Age: 30} + rfx := New(data) + + if rfx.Get("Name").String() != "Alice" { + t.Errorf("Expected 'Alice', got '%s'", rfx.Get("Name").String()) + } + }) + + t.Run("New with interface wrapping pointer", func(t *testing.T) { + // 测试 interface{} 包装的指针 + person := Person{Name: "Alice", Age: 30} + var data any = &person + rfx := New(data) + + rfx.Set("Name", "Bob") + // 原始数据应该被修改(因为是指针) + if person.Name != "Bob" { + t.Errorf("Expected original to be modified, got '%s'", person.Name) + } + }) + + t.Run("New with nil interface", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic with nil interface") + } + }() + var data any + New(data) + }) } // TestNewWithPointer 测试传入指针的情况