Files
reflux/README.md
T
what 50f5531d3b docs: 讲清"值传递的开销只在 New 这一次", 并补上浅拷贝/深拷贝的分岔
两处之前没说清楚的地方。

一、值传递的代价只发生在构造那一次, 不影响之后的操作

原来只列了 New(值) 3227ns vs New(&v) 25ns, 容易被读成"传值之后一直慢"。
实际上构造完成后两者的内部表示完全相同(都是类型描述符 + 地址), 后续
Get/Set 走同一条代码路径:

                     构造后 Get          构造后 Set
  New(v)  传值构造    49.7ns / 2 allocs   27.2ns / 0 allocs
  New(&v) 传指针构造  50.3ns / 2 allocs   27.1ns / 0 allocs

差异在噪声范围内。所以要看的是**构造频次**而不是访问频次: 构造一次访问很多次
的话那点开销会被摊薄; 每个请求都构造、对象又含 map/slice 时才值得计较。

二、流程图补上值分支内部的二次分岔

图是在 needsClone 那次优化之前画的, 一直写着"传值 = DeepClone", 已经不准:

  不含引用成分(纯值 struct/数组/字符串) -> 逐字节浅拷贝, 约 85 ns
    字符串虽然内部有指针, 但底层数组不可变, 共享是安全的
  含指针/切片/map/interface(递归包含字段与数组元素) -> DeepClone

这也是"能不能 copy 一份再取指针"的答案: 纯值类型可以(库里已经这么做了),
含引用的类型不行 —— 浅拷贝只复制 slice/map 的头部, 底层数据仍与调用方共享,
写入会穿透, 那就不再是隔离副本了。

顺带把代价数字更正为实测值(原来写的 1518ns/44allocs 是另一个更小的样本)。
2026-08-31 14:52:23 +08:00

1380 lines
38 KiB
Markdown

# Reflux
Reflux 是一个 Go 语言包,提供了统一的接口用于访问和操作嵌套的结构体字段、切片元素和映射值。通过字符串路径的方式,可以方便地访问和修改深层嵌套的数据结构。
## 特性
- 🔍 **统一访问**: 使用字符串路径访问结构体、Map、切片中的任意嵌套值
- 🎯 **点号路径**: 支持点号分割的路径语法,如 `Get("Address.City")`
- 🔄 **类型转换**: 基于 [spf13/cast](https://github.com/spf13/cast) 的强大类型转换支持
- ✏️ **修改数据**: 支持通过路径设置和删除值
- 🔗 **链式调用**: Set 和 Delete 方法支持链式调用,如 `rfx.Set("name", "Alice").Delete("age").Set("city", "Beijing")`
- 📋 **深度克隆**: Scope 方法和值传递模式创建深度克隆,修改不影响原始数据
- 🎭 **灵活模式**: 支持指针和值两种传递方式,提供不同的数据操作语义
- 🔌 **Interface 支持**: 自动解析 `interface{}` 类型到实际类型
- 🔤 **大小写不敏感**: 支持使用小写字段名访问和修改结构体字段(包括 Map 键名)
- 🔑 **键名提取**: Keys 方法可获取结构体或 Map 的所有键名
- 📝 **JSON 集成**: Ptr() 方法返回的指针可直接用于 json.Unmarshal(),实现动态 JSON 反序列化
- 🎯 **JSON 序列化**: 实现 json.Marshaler 和 json.Unmarshaler 接口,支持直接序列化和反序列化 R 实例
- 🎯 **类型安全**: 使用反射但保证类型安全
- 🔥 **增强类型转换**: 支持切片和结构体之间的智能转换(如 []any -> []T, map -> struct)
- 🌀 **R 接口集成**: 支持直接传入 R 接口或 []R 切片,无缝集成反射值
-**泛型直取**: `reflux.Get[string](r, "Address", "City")` 直接返回目标类型,**零内存分配**
- 🚀 **高性能**: 类型布局缓存 + 指针偏移寻址,Get/Set 比逐次反射快 3-4 倍,访问器快 8-12 倍
- 📦 **零依赖**: 仅依赖 Go 标准库和 spf13/cast
## 安装
```bash
go get git.fsdpf.net/go/reflux
```
## 快速开始
```go
package main
import (
"fmt"
"git.fsdpf.net/go/reflux"
)
type User struct {
Name string
Age int
Address struct {
City string
}
}
func main() {
user := User{
Name: "Alice",
Age: 30,
}
user.Address.City = "Beijing"
rfx := reflux.New(&user)
// 获取值
name := rfx.Get("Name").String()
age := rfx.Get("Age").Int()
city := rfx.Get("Address", "City").String()
fmt.Printf("Name: %s, Age: %d, City: %s\n", name, age, city)
// 链式设置值
rfx.Set("Name", "Bob").Set("Age", 35).Set("Address.City", "Shanghai")
fmt.Printf("Updated: %+v\n", user)
}
```
## 核心功能
### 1. 创建 Reflux
Reflux 支持**指针**和**值**两种传递方式,提供不同的数据操作语义:
#### 指针模式 - 直接修改原始数据
```go
user := User{Name: "Alice"}
rfx := reflux.New(&user) // 传入指针
rfx.Set("Name", "Bob")
// user.Name 已被修改为 "Bob"
```
#### 值模式 - 深度克隆,不影响原始数据
```go
user := User{Name: "Alice"}
rfx := reflux.New(user) // 传入值
rfx.Set("Name", "Bob")
// user.Name 仍然是 "Alice"
// rfx 内部是独立的深度克隆
```
#### 该传值还是传指针
**默认传值是安全的,但深度克隆的代价随数据规模增长**:
| 输入 | `New(v)` 传值 | `New(&v)` 传指针 | 差距 |
|---|---|---|---:|
| 10 键嵌套 map | 3227 ns / 119 allocs | 24.8 ns / 1 alloc | **130x** |
| 50 元素 slice | 594 ns / 6 allocs | 25 ns / 1 alloc | **24x** |
| 8 字段纯值 struct | 85.5 ns / 2 allocs | 25.5 ns / 1 alloc | 3.4x |
**注意这笔开销只发生在 `New` 这一次。** 构造完成后两者的内部表示完全相同,
后续 `Get`/`Set` 走同一条代码路径,速度没有差别:
| | 构造后 `Get` | 构造后 `Set` |
|---|---|---|
| `New(v)` 传值构造 | 49.7 ns / 2 allocs | 27.2 ns / 0 allocs |
| `New(&v)` 传指针构造 | 50.3 ns / 2 allocs | 27.1 ns / 0 allocs |
所以要看的是**构造频次**而不是访问频次:构造一次访问很多次的话,
传值那点开销会被摊薄;每个请求都构造、对象又含 map/slice 时才值得计较。
至于选哪个,标准就一句:**这份数据我会不会通过 R 去写?写了穿透出去有没有问题?**
- 只读,或者本来就想改调用方的数据 → **传指针**
- 需要一份互不干扰的副本 → 传值(或者传指针之后用 `Scope()` 取局部副本)
取不到地址的表达式,先落一个局部变量:
```go
// 取不到地址
return reflux.New(lo.Assign(a, b))
// 落一个变量, 顺带让"共享数据"这件事在调用点就看得见
m := lo.Assign(a, b)
return reflux.New(&m)
```
纯值类型(不含指针/slice/map/interface)的传值成本已经优化过 —— 逐字节复制
就已经完全独立,不走深拷贝的递归。所以上表里 struct 那一行差距最小。
#### 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)
```go
// 获取顶层字段
name := rfx.Get("Name").String()
// 获取嵌套字段 - 使用多个参数
city := rfx.Get("Address", "City").String()
// 获取嵌套字段 - 使用点号路径
city := rfx.Get("Address.City").String()
// 深层嵌套 - 点号路径
ceoCity := rfx.Get("Company.CEO.Address.City").String()
// 混合使用点号和参数
value := rfx.Get("User.Profile", "Settings", "Theme").String()
// 获取切片元素 - 使用索引
tag := rfx.Get("Tags", "0").String()
// 获取切片元素 - 使用点号
tag := rfx.Get("Tags.0").String()
// 获取 Map 值 - 使用多个参数
value := rfx.Get("Meta", "key").String()
// 获取 Map 值 - 使用点号
value := rfx.Get("Meta.key").String()
// 大小写不敏感访问 - 结构体字段和 Map 键名
name := rfx.Get("name").String() // 等同于 Get("Name")
city := rfx.Get("address.city").String() // 等同于 Get("Address.City")
// Map 类型也支持大小写转换
config := map[string]string{"Host": "localhost"}
rfx := reflux.New(&config)
host := rfx.Get("host").String() // 自动尝试 "Host"
```
### 3. 作用域 (Scope)
`Scope` 方法创建指定路径的**深度克隆**,在克隆上的修改不会影响原始数据:
```go
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 := reflux.New(&person)
// 创建 Address 字段的深度克隆
addressScope := rfx.Scope("Address")
// 在克隆上修改值
addressScope.Set("City", "Shanghai")
addressScope.Set("Street", "New St")
// 原始数据不受影响
fmt.Println(person.Address.City) // 输出: Beijing
fmt.Println(person.Address.Street) // 输出: Main St
// 克隆数据已修改
fmt.Println(addressScope.Get("City").String()) // 输出: Shanghai
fmt.Println(addressScope.Get("Street").String()) // 输出: New St
// 不传参数时,克隆整个对象
clone := rfx.Scope()
clone.Set("Name", "Bob")
fmt.Println(person.Name) // 输出: Alice (原始数据不变)
```
**重要**: `Scope` 返回的是深度克隆,与原始数据完全独立。
### 4. 设置值 (Set)
`Set` 方法使用新的 API 签名: `Set(key string, v any) Reflux`,支持链式调用:
```go
// 链式设置多个值
rfx.Set("Name", "Bob").Set("Age", 35).Set("Address.City", "Shanghai")
// 设置顶层字段
rfx.Set("Name", "Bob")
// 设置嵌套字段 - 使用点号路径
rfx.Set("Address.City", "Shanghai")
// 深层嵌套设置
rfx.Set("Company.CEO.Address.City", "Guangzhou")
// 设置 Map 值
rfx.Set("Meta.key", "value")
// 设置切片元素
rfx.Set("Tags.0", "newValue")
// 大小写不敏感设置
rfx.Set("name", "Bob") // 等同于 Set("Name", "Bob")
rfx.Set("address.city", "Shanghai") // 等同于 Set("Address.City", "Shanghai")
// 自动类型转换
rfx.Set("Age", int32(35)) // int32 -> int
// 切片类型转换 - 从通用切片转为具体类型
rfx.Set("Scores", []any{90, 95, 88}) // []any -> []int
// 结构体类型转换 - 从 map 填充到 struct
addressMap := map[string]any{
"City": "Shanghai",
"Street": "Nanjing Road",
}
rfx.Set("Address", addressMap) // map -> Address struct
// 指针切片赋值 - 保持指针引用
type Item struct {
Name string
}
item1 := &Item{Name: "apple"}
item2 := &Item{Name: "banana"}
rfx.Set("Items", []*Item{item1, item2}) // 保持原指针地址
// 也支持从 []any 包含指针元素
rfx.Set("Items", []any{item1, item2}) // 复用指针地址
// R 接口和 []R 切片支持
rfx1 := reflux.New(data1)
rfx2 := reflux.New(data2)
rfx.Set("Item", rfx1) // 直接传入 R 接口
rfx.Set("Items", []R{rfx1, rfx2}) // 传入 R 切片
// Map 自动初始化
rfx.Set("NewMap.key", "value") // 如果 NewMap 是 nil,会自动初始化
```
**注意**: 如果设置失败(路径不存在、类型不匹配等),会 panic。
#### 高级类型转换
Reflux 提供了强大的智能类型转换能力,支持多种复杂场景:
**1. 切片类型转换**
支持从通用切片(如 `[]any`)转换为具体类型切片:
```go
type Person struct {
Scores []int
Tags []string
}
p := Person{}
rfx := reflux.New(&p)
// []any -> []int
rfx.Set("Scores", []any{90, 95, 88})
// p.Scores = []int{90, 95, 88}
// []any -> []string (自动类型转换)
rfx.Set("Tags", []any{"go", "rust", 123})
// p.Tags = []string{"go", "rust", "123"}
```
**2. 结构体类型转换**
支持从 map 或其他结构体填充到目标结构体:
```go
type Address struct {
City string
Street string
}
type Person struct {
Address Address
}
p := Person{}
rfx := reflux.New(&p)
// map -> struct
addressMap := map[string]any{
"City": "Shanghai",
"Street": "Nanjing Road",
}
rfx.Set("Address", addressMap)
// p.Address = Address{City: "Shanghai", Street: "Nanjing Road"}
// struct -> struct (按字段名匹配)
src := Address{City: "Beijing", Street: "Changan"}
rfx.Set("Address", src)
```
**3. 指针切片处理**
对于包含指针的切片,Reflux 会保持指针引用:
```go
type Fruit struct {
Name string
}
type Basket struct {
Fruits []*Fruit
}
basket := Basket{}
rfx := reflux.New(&basket)
apple := &Fruit{Name: "apple"}
banana := &Fruit{Name: "banana"}
// 保持指针引用,不创建新对象
rfx.Set("Fruits", []*Fruit{apple, banana})
// 修改原指针对象,basket.Fruits 中会看到相同变化
apple.Name = "green apple"
// basket.Fruits[0].Name == "green apple"
// 也支持从 []any 赋值,复用指针地址
rfx.Set("Fruits", []any{apple, banana})
```
**4. R 接口集成**
支持直接传入 R 接口或 []R 切片:
```go
// 传入单个 R 接口
data1 := map[string]string{"key": "value"}
rfx1 := reflux.New(data1)
rfx.Set("Item", rfx1) // 自动提取底层值
// 传入 []R 切片
data2 := map[string]int{"count": 10}
rfx2 := reflux.New(data2)
rfx.Set("Items", []R{rfx1, rfx2}) // 自动转换为底层切片
```
**5. reflect.Value 支持**
可以直接传入 `reflect.Value`,避免重复封装:
```go
import "reflect"
val := reflect.ValueOf(someData)
rfx.Set("Field", val) // 直接使用,不重复包装
```
这些高级转换特性使得 Reflux 在处理复杂数据结构和动态类型场景时更加灵活和强大。
#### 切片追加 (Append)
`Append` 用于在当前 `R` 对应的切片上追加一个或多个元素,并支持自动类型转换:
```go
// 追加到顶层切片
items := []string{"apple", "banana"}
rfx := reflux.New(&items)
// 追加单个和多个元素
rfx.Append("cherry")
rfx.Append("durian", "kiwi")
// items == []string{"apple", "banana", "cherry", "durian", "kiwi"}
// 使用索引 -1 在切片前面插入新元素
// 相当于在开头插入,原有元素整体后移
rfx.Set("-1", "first")
// 对于上面的 items,现在结果为:
// items == []string{"first", "apple", "banana", "cherry", "durian", "kiwi"}
// 追加时支持类型转换
nums := []int{1, 2}
rfxNums := reflux.New(&nums)
rfxNums.Append("3", 4.0) // "3" -> 3, 4.0 -> 4
// nums == []int{1, 2, 3, 4}
// 追加到嵌套切片字段
type Container struct {
Tags []string
}
c := Container{Tags: []string{"go"}}
rfxContainer := reflux.New(&c)
rfxContainer.Get("Tags").Append("rust", "python")
// c.Tags == []string{"go", "rust", "python"}
```
**注意**:
- 只能对**切片类型**调用 `Append`,否则会 panic
- 追加多个值时会一次性追加,避免多次扩容
### 5. 删除值 (Delete)
`Delete` 方法支持链式调用,返回 Reflux 自身:
```go
// 删除 Map 键 - 使用多个参数
rfx.Delete("Meta", "key")
// 删除 Map 键 - 使用点号
rfx.Delete("Meta.key")
// 删除切片元素 - 使用索引
rfx.Delete("Tags", "1") // 删除索引 1 的元素
// 删除切片元素 - 使用点号
rfx.Delete("Tags.1")
// 链式调用 - 删除多个值
rfx.Delete("Meta.key1").Delete("Meta.key2").Set("Meta.key3", "value")
```
**注意**: 如果删除失败(路径不存在、类型不支持删除等),会 panic。
### 6. 检查存在性 (Exists)
```go
if rfx.Exists("Name") {
fmt.Println("Name field exists")
}
// 使用多个参数检查嵌套字段
if rfx.Exists("Address", "City") {
fmt.Println("Nested field exists")
}
// 使用点号路径检查嵌套字段
if rfx.Exists("Address.City") {
fmt.Println("Nested field exists")
}
// 检查深层嵌套
if rfx.Exists("Company.CEO.Address.City") {
fmt.Println("Deep nested field exists")
}
```
### 7. 获取键名 (Keys)
```go
type User struct {
Name string
Age int
}
user := User{Name: "Alice", Age: 30}
rfx := reflux.New(&user)
// 获取结构体的所有字段名
keys := rfx.Keys()
fmt.Println(keys) // 输出: [Name Age]
// 对于 Map
config := map[string]any{
"host": "localhost",
"port": 8080,
}
sm2 := reflux.New(&config)
keys2 := sm2.Keys()
fmt.Println(keys2) // 输出: [host port]
// 获取嵌套对象的键名
addressKeys := rfx.Get("Address").Keys()
```
### 8. 数组操作 (Array)
```go
// 将切片转换为 Reflux 数组
tags := rfx.Get("Tags").Array()
for i, tag := range tags {
fmt.Printf("Tag[%d]: %s\n", i, tag.String())
}
// 可以对数组元素进行进一步操作
users := rfx.Get("Users").Array()
for i, user := range users {
name := user.Get("Name").String()
age := user.Get("Age").Int()
fmt.Printf("User[%d]: %s, %d\n", i, name, age)
}
```
### 9. 类型转换
#### 基本类型
```go
age := rfx.Get("Age").Int() // int
age64 := rfx.Get("Age").Int64() // int64
age32 := rfx.Get("Age").Int32() // int32
age16 := rfx.Get("Age").Int16() // int16
age8 := rfx.Get("Age").Int8() // int8
uage := rfx.Get("Age").Uint() // uint
uage64 := rfx.Get("Age").Uint64() // uint64
uage32 := rfx.Get("Age").Uint32() // uint32
uage16 := rfx.Get("Age").Uint16() // uint16
uage8 := rfx.Get("Age").Uint8() // uint8
fage := rfx.Get("Age").Float64() // float64
fage32 := rfx.Get("Age").Float32() // float32
sage := rfx.Get("Age").String() // string
active := rfx.Get("Active").Bool() // bool
```
**注意**: 类型转换使用 [spf13/cast](https://github.com/spf13/cast) 库,支持智能类型转换。转换失败会 panic。
#### Map 类型
```go
// map[string]string
strMap := rfx.Get("Meta").StringMapString()
// map[string]int
intMap := rfx.Get("Scores").StringMapInt()
// map[string]int64
int64Map := rfx.Get("Scores").StringMapInt64()
// map[string]bool
boolMap := rfx.Get("Flags").StringMapBool()
// map[string]any
anyMap := rfx.Get("Data").StringMap()
// map[string][]string
sliceMap := rfx.Get("Tags").StringMapStringSlice()
```
#### 切片类型
```go
// []string
tags := rfx.Get("Tags").StringSlice()
// []int
scores := rfx.Get("Scores").IntSlice()
// []bool
flags := rfx.Get("Flags").BoolSlice()
// []any
items := rfx.Get("Items").Slice()
```
### 10. 底层访问
```go
// 获取 reflect.Value
val := rfx.Get("Name").Value()
// 获取指针
ptr := rfx.Get("Name").Ptr()
// 获取 any 类型
any := rfx.Get("Name").Any()
```
#### 与 json.Unmarshal() 配合使用
`Ptr()` 方法返回的指针可以直接用于 `json.Unmarshal()`,实现动态的 JSON 反序列化:
```go
import (
"encoding/json"
"git.fsdpf.net/go/reflux"
)
type Person struct {
Name string
Age int
Address Address
Meta map[string]string
Tags []string
}
type Address struct {
City string
Street string
ZipCode int
}
func main() {
p := Person{
Name: "Alice",
Address: Address{City: "OldCity"},
Meta: make(map[string]string),
}
rfx := reflux.New(&p)
// 1. 更新嵌套结构体
addressJSON := []byte(`{
"City": "Shanghai",
"Street": "Nanjing Road",
"ZipCode": 200000
}`)
json.Unmarshal(addressJSON, rfx.Get("Address").Ptr())
// p.Address 已被完整更新
// 2. 更新 Map 字段
metaJSON := []byte(`{
"key1": "value1",
"key2": "value2"
}`)
json.Unmarshal(metaJSON, rfx.Get("Meta").Ptr())
// p.Meta 已被填充
// 3. 更新切片字段
tagsJSON := []byte(`["go", "rust", "python"]`)
json.Unmarshal(tagsJSON, rfx.Get("Tags").Ptr())
// p.Tags 已被替换
// 4. 更新整个对象
personJSON := []byte(`{
"Name": "Bob",
"Age": 35
}`)
json.Unmarshal(personJSON, rfx.Ptr())
// 整个 Person 对象被更新
}
```
**使用场景**:
- 动态配置更新: 从 JSON 文件或 API 响应更新配置的特定部分
- 部分数据刷新: 只更新对象的某个嵌套字段,其他字段保持不变
- 插件系统: 动态加载和更新插件配置
- 热更新: 在运行时更新应用配置而无需重启
### 11. JSON 序列化和反序列化
R 接口实现了 `json.Marshaler``json.Unmarshaler` 接口,支持直接对 R 实例进行 JSON 序列化和反序列化:
#### JSON 序列化 (MarshalJSON)
```go
import (
"encoding/json"
"git.fsdpf.net/go/reflux"
)
type Person struct {
Name string
Age int
Tags []string
}
func main() {
person := Person{
Name: "Alice",
Age: 30,
Tags: []string{"developer", "golang"},
}
rfx := reflux.New(&person)
// 直接序列化 R 实例
data, err := json.Marshal(rfx)
if err != nil {
panic(err)
}
fmt.Println(string(data))
// 输出: {"Name":"Alice","Age":30,"Tags":["developer","golang"]}
}
```
#### JSON 反序列化 (UnmarshalJSON)
```go
func main() {
person := Person{}
rfx := reflux.New(&person)
jsonData := []byte(`{"Name":"Bob","Age":35,"Tags":["manager","python"]}`)
// 直接反序列化到 R 实例
err := json.Unmarshal(jsonData, rfx)
if err != nil {
panic(err)
}
// 数据已更新到原始对象
fmt.Printf("%+v\n", person)
// 输出: {Name:Bob Age:35 Tags:[manager python]}
// 也可以通过 R 接口访问
fmt.Println(rfx.Get("Name").String()) // 输出: Bob
fmt.Println(rfx.Get("Age").Int()) // 输出: 35
}
```
#### JSON 往返转换
```go
func main() {
// 原始数据
original := map[string]any{
"name": "Charlie",
"age": 40,
"active": true,
}
rfx1 := reflux.New(&original)
// 序列化
data, _ := json.Marshal(rfx1)
// 反序列化到新对象
result := make(map[string]any)
rfx2 := reflux.New(&result)
json.Unmarshal(data, rfx2)
// 验证数据一致性
fmt.Println(rfx2.Get("name").String()) // Charlie
fmt.Println(rfx2.Get("age").Float64()) // 40 (JSON 数字默认 float64)
fmt.Println(rfx2.Get("active").Bool()) // true
}
```
#### 嵌套结构序列化
```go
type Address struct {
City string
Country string
}
type User struct {
Name string
Age int
Address Address
}
func main() {
user := User{
Name: "David",
Age: 45,
Address: Address{
City: "Beijing",
Country: "China",
},
}
rfx := reflux.New(&user)
// 序列化整个嵌套结构
data, _ := json.Marshal(rfx)
fmt.Println(string(data))
// 输出: {"Name":"David","Age":45,"Address":{"City":"Beijing","Country":"China"}}
// 只序列化某个嵌套字段
addressData, _ := json.Marshal(rfx.Get("Address"))
fmt.Println(string(addressData))
// 输出: {"City":"Beijing","Country":"China"}
}
```
#### 智能类型转换
反序列化时,R 接口会自动进行类型转换:
```go
type Config struct {
Port int
Timeout int64
Enabled bool
}
func main() {
config := Config{}
rfx := reflux.New(&config)
// JSON 中的数字默认是 float64
jsonData := []byte(`{"Port":8080,"Timeout":30,"Enabled":true}`)
json.Unmarshal(jsonData, rfx)
// 自动转换为目标类型
fmt.Printf("Port: %d (type: int)\n", config.Port) // 8080
fmt.Printf("Timeout: %d (type: int64)\n", config.Timeout) // 30
fmt.Printf("Enabled: %v (type: bool)\n", config.Enabled) // true
}
```
**使用场景**:
- **API 通信**: 直接序列化 R 实例发送到 HTTP API
- **配置持久化**: 将配置对象保存为 JSON 文件
- **数据传输**: 在不同系统间传输复杂数据结构
- **缓存系统**: 将对象序列化后存储到 Redis 等缓存
- **消息队列**: 序列化后通过消息队列传输
**注意事项**:
- 序列化会调用底层 `Any()` 方法获取实际值
- 反序列化支持智能类型转换,使用 `setValue` 方法
- 对于 struct 类型,会按字段名匹配反序列化
- JSON 数字默认解析为 `float64`,会自动转换为目标类型
## 使用场景
### 1. 配置文件处理
```go
type Config struct {
Database struct {
Host string
Port int
}
Redis struct {
Addr string
}
}
config := loadConfig()
rfx := reflux.New(&config)
// 动态读取配置
dbHost := rfx.Get("Database", "Host").String()
dbPort := rfx.Get("Database", "Port").Int()
// 链式修改配置
rfx.Set("Database.Host", "localhost").Set("Database.Port", 3306)
```
### 2. API 响应处理
```go
response := map[string]any{
"user": map[string]any{
"name": "Alice",
"age": 30,
},
"status": "success",
}
rfx := reflux.New(&response)
userName := rfx.Get("user", "name").String()
userAge := rfx.Get("user", "age").Int()
// 修改响应数据
rfx.Set("user.name", "Bob").Set("status", "updated")
```
### 3. 动态表单数据
```go
formData := map[string]any{
"name": "Bob",
"email": "bob@example.com",
"age": "25", // 字符串形式的数字
}
rfx := reflux.New(&formData)
// 自动类型转换
name := rfx.Get("name").String()
age := rfx.Get("age").Int() // "25" -> 25
// 验证和修改
if age < 18 {
rfx.Set("verified", false)
}
```
### 4. 测试数据构建
```go
testUser := User{}
rfx := reflux.New(&testUser)
// 链式快速设置测试数据
rfx.Set("Name", "TestUser").
Set("Age", 25).
Set("Address.City", "Beijing").
Set("Address.Street", "Test St")
```
### 5. 数据克隆和隔离
```go
original := Config{Host: "localhost"}
rfx := reflux.New(&original)
// 创建深度克隆进行测试
testConfig := rfx.Scope()
testConfig.Set("Host", "test-server").Set("Port", 9999)
// 原始配置不受影响
fmt.Println(original.Host) // 输出: localhost
```
### 6. JSON 动态更新
使用 `Ptr()` 方法配合 `json.Unmarshal()` 实现动态更新:
```go
type AppConfig struct {
Server ServerConfig
Database DatabaseConfig
Features map[string]bool
}
type ServerConfig struct {
Host string
Port int
}
type DatabaseConfig struct {
Driver string
DSN string
}
func main() {
config := AppConfig{
Server: ServerConfig{
Host: "localhost",
Port: 8080,
},
Features: make(map[string]bool),
}
rfx := reflux.New(&config)
// 从配置文件或 API 只更新 Server 配置
serverJSON := []byte(`{
"Host": "production.example.com",
"Port": 443
}`)
json.Unmarshal(serverJSON, rfx.Get("Server").Ptr())
// 动态启用功能开关
featuresJSON := []byte(`{
"newFeature": true,
"experimentalUI": false
}`)
json.Unmarshal(featuresJSON, rfx.Get("Features").Ptr())
// config.Server 已更新,config.Database 保持不变
fmt.Printf("%+v\n", config)
}
```
这种方式特别适合:
- **微服务配置**: 从配置中心动态更新特定模块配置
- **A/B 测试**: 实时更新功能开关
- **插件热加载**: 更新插件配置而无需重启
- **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 支持灵活的点号路径语法:
```go
// 以下调用是等价的:
rfx.Get("a.b.c")
rfx.Get("a.b", "c")
rfx.Get("a", "b.c")
rfx.Get("a", "b", "c")
// 空段会被忽略
rfx.Get("a..b") // 等价于 rfx.Get("a", "b")
rfx.Get(".a.b.") // 等价于 rfx.Get("a", "b")
// 点号路径适用于所有方法
rfx.Set("a.b.c", value)
rfx.Delete("a.b.c")
rfx.Exists("a.b.c")
rfx.Scope("a.b.c")
// 访问切片元素
tag := rfx.Get("Tags.0").String()
// 访问 Map 值
value := rfx.Get("Config.Database.Host").String()
```
## API 文档
### 包级函数 Get[T] (泛型直取)
```go
func Get[T any](r R, path ...string) T
```
按路径取值并直接返回目标类型,**不产生中间的 R 包装对象**。
```go
p := &Person{Name: "Alice", Address: Address{City: "Beijing"}}
r := reflux.New(p)
city := reflux.Get[string](r, "Address", "City") // "Beijing"
city2 := reflux.Get[string](r, "Address.City") // 点号路径同样可用
age := reflux.Get[int](r, "Age")
ok := reflux.Get[bool](r, "Active")
ratio := reflux.Get[float64](r, "Ratio")
```
语义与 `r.Get(path...).Xxx()` **严格等价**,可以放心替换:
| 情况 | 行为 |
|---|---|
| 路径不存在 / 未导出字段 / 下标越界 | 返回 `T` 的零值 |
| 类型转换失败 | panic,错误信息与访问器方法完全一致 |
| 传入非本包实现的 `R` | 自动回退到 `r.Get(path...).Xxx()`,结果一致 |
差别只在开销: 链式写法每次都要在堆上新建一个 `R` 包装对象,
`Get[T]` 直接把结果写进调用方的变量。
```go
r.Get("Address", "City").String() // 2 次分配
reflux.Get[string](r, "Address", "City") // 0 次分配, 快约 1.7 倍
```
**覆盖类型**: `T` 支持 `valuex.Accessor` 全部转换方法对应的类型,每个分支都落到
同名访问器上,因此**转换语义完全一致**:
| 类别 | 支持的 T |
|---|---|
| 零分配快路径 | `string` `int` `int64` `bool` `float64` |
| 其余标量 | `int8` `int16` `int32` `uint` `uint8` `uint16` `uint32` `uint64` `float32` |
| 切片 | `[]any` `[]string` `[]int` `[]bool` |
| map | `map[string]any` `map[string]string` `map[string]int` `map[string]int64` `map[string]bool` `map[string][]string` |
因为落到访问器上,所以**会做转换**而不是类型断言:
```go
type Doc struct{ Tags []any; Age int }
d := &Doc{Tags: []any{"a", "b"}, Age: 42}
r := reflux.New(d)
reflux.Get[[]string](r, "Tags") // ["a" "b"] —— 逐元素转换, 不是断言失败返回 nil
reflux.Get[int32](r, "Age") // 42 —— 与 r.Get("Age").Int32() 一致
```
**两点注意**:
1. `T` 只出现在返回值里,Go 无法类型推导,必须显式写出 `Get[string](...)`
2. **表格之外的类型**走 `Any().(T)` 断言,**不做转换** —— 这对
`Get[SomeStruct](r, "Field")` 这种"取出原样的值"是有用的,但具名标量类型
(`type MyStr string`)在字段是原生 `string` 时会断言失败返回零值,
这种场景请改用 `r.Get("Name").String()`
### Reflux 接口
```go
type Reflux interface {
// 路径操作
Get(p ...string) Reflux
Scope(p ...string) Reflux
// 修改操作 (支持链式调用)
Set(key string, v any) Reflux
Delete(p ...string) Reflux
Exists(p ...string) bool
// 数组和键操作
Array() []Reflux
Keys() []string
// 底层访问
Value() reflect.Value
Ptr() any
Any() any
// 基本类型转换
Bool() bool
Int() int
Int8() int8
Int16() int16
Int32() int32
Int64() int64
Uint() uint
Uint8() uint8
Uint16() uint16
Uint32() uint32
Uint64() uint64
Float32() float32
Float64() float64
String() string
// Map 类型转换
StringMapString() map[string]string
StringMapStringSlice() map[string][]string
StringMapBool() map[string]bool
StringMapInt() map[string]int
StringMapInt64() map[string]int64
StringMap() map[string]any
// Slice 类型转换
Slice() []any
BoolSlice() []bool
StringSlice() []string
IntSlice() []int
}
```
## 性能
> **想看内部是怎么工作的?** [docs/flow.md](docs/flow.md) 用两张流程图讲清了
> `Get`/`Set` 如何把路径逐段分派到 unsafe 快路径或 reflect 回退,
> 以及每条分支各自的代价。下面只讲结论。
### 实现方式
热路径不再逐次走 `reflect` 的按名字段查找,而是:
1. **类型布局缓存** —— 第一次遇到某个类型时,把它每个字段的**字节偏移量**、
元素大小等信息构建成描述符,存进全局缓存(`sync.Map`)。之后同类型直接命中,
字段查找从"按名字线性比较"变成 O(1) 的 map 查表。
2. **指针偏移寻址** —— 取字段时用 `基址 + 偏移量` 直接算出地址,不再构造中间的
`reflect.Value`
3. **零分配路径解析** —— 路径字符串按需切片遍历,不再为每次 `Get` 分配临时切片。
4. **标量直读** —— `String()`/`Int()`/`Bool()`/`Float64()` 等在类型匹配时直接按
机器类型读内存,绕开 `interface{}` 装箱和 `cast` 转换。
语义复杂、调用频次低的操作(复合类型赋值、`Append``Delete`、容器转换)
仍然走原来的 reflect 实现 —— 这些操作的语义琐碎,重写必然引入偏差,
而它们本来就不在性能热点上。
### 与旧版本(纯 reflect 实现)的对比
同一进程、同一数据结构、同一路径,`-benchmem -count=6` 取中位数
(Apple M4 Pro / darwin-arm64 / go1.25.5):
| 场景 | 旧版本 | 新版本 | 提速 |
|---|---|---|---:|
| `Get("Address","City").String()` | 150.6 ns / 152 B / 7 allocs | **50.7 ns / 56 B / 2 allocs** | **2.97x** |
| `Get("Address.City").String()` | 143.2 ns / 136 B / 6 allocs | **52.4 ns / 40 B / 2 allocs** | **2.73x** |
| `Get` 4 层深路径 | 234.1 ns / 280 B / 10 allocs | **68.9 ns / 88 B / 2 allocs** | **3.40x** |
| `Get("Tags","1")` slice 下标 | 134.9 ns / 152 B / 7 allocs | **47.9 ns / 56 B / 2 allocs** | **2.81x** |
| `Set("Name", ...)` | 65.8 ns / 32 B / 2 allocs | **18.1 ns / 0 B / 0 allocs** | **3.64x** |
| `Set("Address.City", ...)` | 122.1 ns / 80 B / 3 allocs | **35.5 ns / 0 B / 0 allocs** | **3.44x** |
| `Exists("Address","City")` | 127.4 ns / 112 B / 5 allocs | **37.2 ns / 32 B / 1 alloc** | **3.42x** |
| 访问器 `String()` (纯转换) | 15.0 ns / 16 B / 1 alloc | **1.3 ns / 0 B / 0 allocs** | **11.8x** |
| 访问器 `Int()` (纯转换) | 12.9 ns / 8 B / 1 alloc | **1.5 ns / 0 B / 0 allocs** | **8.5x** |
泛型直取(新增 API,旧版本没有对应写法):
| 场景 | 新版本 | 相对旧版链式 |
|---|---|---:|
| `Get[string](r, "Address", "City")` | **29.9 ns / 0 B / 0 allocs** | **5.04x** |
| `Get[string](r, "B","C","D","Leaf")` | **45.0 ns / 0 B / 0 allocs** | **5.20x** |
参照基准线: 纯 Go 字段访问 `p.Address.City` 是 0.34 ns / 0 allocs。
### 两处不快的地方(如实说明)
| 场景 | 旧版本 | 新版本 | 变化 |
|---|---|---|---:|
| `Get("Meta","k")` struct 字段 → map 键 | 175.6 ns / 10 allocs | 140.9 ns / 7 allocs | 1.25x |
| **纯 map 路径** `Get("leaf")`(根就是 map) | 120.0 ns / 8 allocs | 126.8 ns / 7 allocs | **0.95x** |
| 纯 map 三层 | 300.9 ns / 20 allocs | 260.8 ns / 15 allocs | 1.15x |
| map → struct → struct | 224.6 ns / 11 allocs | 140.3 ns / 6 allocs | 1.60x |
| `New(指针)` 构造 | 16.9 ns / 1 alloc | 25.4 ns / 1 alloc | **0.67x** |
- **map 路径基本打平,提速有限**。map 没有稳定的内存布局可以做偏移量运算,
这条路径完全走 reflect。命中 map 之后会一次性用 reflect 走完剩余路径、
只在最后装箱一次(逐跳装箱的话每跳都要 `reflect.New` 拷贝一份,因为 map
元素不可寻址),所以层级越深收益越明显;但单跳 map 省不掉那次
`reflect.NewAt`,仍比旧实现慢约 5%。
**选型建议**: 如果你的数据以 `map[string]any` 为主(比如把数据库查询结果直接
存成 map),这次重写对你收益很小 —— 提速几乎全部集中在 **struct 字段访问**上。
- **`New` 慢了约 8 ns**: 构造时要查一次类型描述符缓存。这是一次性成本,
换来之后每次 `Get`/`Set` 省下 50-100 ns —— 只要构造后至少访问一次就是净赚。
### 内存分配
分配次数的下降往往比 CPU 时间更有意义(GC 压力):
- `Set`: **2 → 0**
- `Get` + 访问器: **7 → 2**
- `Exists`: **5 → 1**
- `Get[T]` 泛型直取: **0**
`Get` 剩下的 2 次分配是 API 形状决定的下限: 一次是返回的 `R` 包装对象(24 字节),
一次是可变参数切片(通过接口调用时逃逸分析穿不透)。想完全避免就用 `Get[T]`
### 类型描述符缓存:什么时候清除
**永不清除,正常情况下也不需要清除。**
缓存的 key 是 `reflect.Type`。程序里**声明**的类型是编译期确定的有限集合,
runtime 为它们创建的 `reflect.Type` 本身就是永久对象,所以缓存大小收敛于
"程序实际用到的类型数",warmup 之后不再增长。
实测占用(描述符 + 字段切片 + 字段名索引):
| 类型数 | 缓存占用 |
|---|---|
| 100 | 约 0.06 MB |
| 1,000 | 约 0.6 MB |
| 10,000 | 约 6 MB |
一个中等项目撑死几百个类型,占用在百 KB 级别。
设计上有意做成**只增不删**:描述符指针一旦发布就永久有效,
`Elem`/`Type` 这些递归引用不需要任何生命周期管理,热路径上也不必做
引用计数或有效性校验。
#### 唯一的例外:`reflect.StructOf` 动态造类型
如果调用方用 `reflect.StructOf` / `reflect.MapOf` 等在**运行时动态生成
形状各异的类型**,再把它们交给 `reflux.New`,缓存就会持续增长。
典型场景是 ORM 按查询字段动态拼 struct 作为扫描目标。
实测(12 字段的动态 struct,1000 个):
| | 每类型占用 | 占比 |
|---|---:|---:|
| Go runtime 的类型元数据 | 1369 B | 41% |
| reflux 的类型描述符 | **1945 B** | **59%** |
| 合计 | 3315 B | |
两点需要说明:
1. **这不是 reflux 特有的问题**`reflect.StructOf` 创建的类型
**Go runtime 自己也永不回收**,泄漏在更底层就已经发生了;
reflux 是在此基础上多加了约 59%。
2. **类型数量通常仍然有界**`reflect.StructOf` 对**相同字段集合**(名称、
类型、tag、**顺序**都相同)返回同一个 `reflect.Type`,所以重复调用不会造新类型。
只有当字段组合本身是无界的(比如允许客户端任意指定 `?fields=a,b,c`),
才会真正持续增长。
如果你的场景确实会动态生成大量不同的 struct 类型,建议:
- **优先在上游收敛**:把字段组合固定成有限的几种,或者在构造类型的那一层
加缓存(既省掉 `reflect.StructOf` 的开销,也自然限住了类型数量)。
顺带一提,`reflect.StructOf` 即使命中 runtime 自己的类型缓存也要
**2μs 上下、30 次左右的分配**,重复调用本身就值得避免。
- **注意字段顺序**:字段集合相同但顺序不同会产生**不同**的类型。
如果拼字段时遍历了 map(Go 的 map 迭代顺序随机),同一个逻辑查询每次都会
造出新类型 —— 这是真正会导致无界增长的 bug,务必用切片保证顺序稳定。
## 注意事项
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,请确保路径和类型正确