perf: 用类型布局缓存 + 指针偏移寻址重写热路径

将实验目录 experiment/fastx 的实现合入主实现,替换原来逐次反射的做法。

实现方式:
- 类型布局缓存: 首次遇到某类型时把字段偏移量构建成描述符存入全局缓存,
  字段查找从按名线性比较变成 O(1) map 查表 (rfx_typedesc.go)
- 指针偏移寻址: 取字段时用 基址+偏移量 直接算地址, 不再构造中间 reflect.Value
  (unsafeptr.go, 全部 unsafe 代码集中在这一个文件)
- 零分配路径解析: 路径按需切片遍历, 不再为每次 Get 分配临时切片 (path.go)
- 标量直读: String/Int/Bool/Float64 在类型匹配时直接按机器类型读内存,
  绕开 interface 装箱和 cast 转换

语义复杂、调用频次低的操作(复合类型赋值、Append、Delete、容器转换)仍走原
reflect 实现, 保留为冷路径 refx (rfx_reflect.go), 避免重写引入行为偏差。

新增包级泛型函数 Get[T](r, path...), 按路径直接取出目标类型, 零内存分配。
语义与 r.Get(path...).Xxx() 严格等价, 不改动任何现有接口。

性能对比(同进程同数据, -benchmem -count=6 中位数):
- Get 嵌套 + String   150.6ns/7allocs -> 50.7ns/2allocs   2.97x
- Get 4 层深路径      234.1ns/10allocs -> 68.9ns/2allocs  3.40x
- Set 单层            65.8ns/2allocs  -> 18.1ns/0allocs   3.64x
- Set 嵌套           122.1ns/3allocs  -> 35.5ns/0allocs   3.44x
- Exists             127.4ns/5allocs  -> 37.2ns/1alloc    3.42x
- 访问器 String        15.0ns/1alloc   -> 1.3ns/0allocs   11.8x
- Get[string] 泛型直取                 -> 29.9ns/0allocs   5.04x
两处不快: map 键访问 1.25x(map 无稳定布局, 仍走 reflect);
New 构造 0.67x(多一次描述符缓存查找, 一次性成本)。

内存安全: 未导出字段在读和写两处显式拦截 —— reflect.NewAt 构造的 Value
不带只读标记, 语言层面的导出规则保护在 unsafe 路径上失效, 必须自己拦。
不变式与评审要点见 unsafeptr.go 顶部注释。

测试: 新增 703 个用例全部通过, 含 go test -race。
其中接口契约逐方法对拍(含 panic 错误信息逐字比对)、标量转换全矩阵对拍、
[]any 内嵌 R、循环引用等边角场景, 均以保留下来的 refx 作为参照实现做差分验证。
合并过程中据此发现并修复 11 处行为偏差。
This commit is contained in:
2026-08-28 16:37:04 +08:00
parent 1f48635da3
commit 5c9f8bd6e7
16 changed files with 4360 additions and 851 deletions
+115 -1
View File
@@ -19,7 +19,8 @@ Reflux 是一个 Go 语言包,提供了统一的接口用于访问和操作嵌
- 🎯 **类型安全**: 使用反射但保证类型安全 - 🎯 **类型安全**: 使用反射但保证类型安全
- 🔥 **增强类型转换**: 支持切片和结构体之间的智能转换(如 []any -> []T, map -> struct) - 🔥 **增强类型转换**: 支持切片和结构体之间的智能转换(如 []any -> []T, map -> struct)
- 🌀 **R 接口集成**: 支持直接传入 R 接口或 []R 切片,无缝集成反射值 - 🌀 **R 接口集成**: 支持直接传入 R 接口或 []R 切片,无缝集成反射值
- 🚀 **高性能**: 优化的反射操作,低内存开销 - **泛型直取**: `reflux.Get[string](r, "Address", "City")` 直接返回目标类型,**零内存分配**
- 🚀 **高性能**: 类型布局缓存 + 指针偏移寻址,Get/Set 比逐次反射快 3~4 倍,访问器快 8~12 倍
- 📦 **零依赖**: 仅依赖 Go 标准库和 spf13/cast - 📦 **零依赖**: 仅依赖 Go 标准库和 spf13/cast
## 安装 ## 安装
@@ -1063,6 +1064,50 @@ value := rfx.Get("Config.Database.Host").String()
## API 文档 ## 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 倍
```
**覆盖类型**: `string` / `int` / `int64` / `bool` / `float64` 走零分配快路径;
其余类型走通用路径,行为等同 `r.Get(path...).Any().(T)`
**两点注意**:
1. `T` 只出现在返回值里,Go 无法类型推导,必须显式写出 `Get[string](...)`
2. 快路径只认原生标量类型。`Get[MyStr](r, "Name")` 在字段是原生 `string` 时,
会走通用路径并因类型断言失败返回零值 —— 这种场景请用 `r.Get("Name").String()`
### Reflux 接口 ### Reflux 接口
```go ```go
@@ -1117,6 +1162,75 @@ type Reflux interface {
} }
``` ```
## 性能
### 实现方式
热路径不再逐次走 `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")` map 键 | 175.6 ns / 10 allocs | 140.9 ns / 7 allocs | 1.25x |
| `New(指针)` 构造 | 16.9 ns / 1 alloc | 25.4 ns / 1 alloc | **0.67x** |
- **map 只快 1.25 倍**: map 没有稳定的内存布局可以做偏移量运算,这条路径完全走
reflect,而且取出来的值必须拷一份(map 元素不可寻址)。这是设计上的取舍。
- **`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]`
## 注意事项 ## 注意事项
1. **指针 vs 值传递**: 1. **指针 vs 值传递**:
+117
View File
@@ -0,0 +1,117 @@
package reflux
import "strings"
// pathIter 在不分配内存的前提下遍历路径片段。
//
// reflux 现有的 expandPath 会对每个片段做 strings.Split 并 append 到一个新切片,
// Get("Address","City") 这样一次调用就要分配 4 次(2 次 Split 的结果切片 +
// append 扩容 2 次)。pathIter 只做字符串切片(sub-slice 不分配)。
type pathIter struct {
parts []string
// one 用于"路径只有一个字符串"的场景(Set/Delete 的 key 参数)。
// 走这条分支可以避免临时构造 []string{key} 带来的堆分配。
one string
isOne bool
i int // 当前处理到第几个片段
off int // 在当前片段内部的字节偏移
n int // 已经产出的段数
limit int // 最多产出多少段, -1 表示不限制
}
func newPathIter(parts []string) pathIter {
return pathIter{parts: parts, limit: -1}
}
func (it *pathIter) numParts() int {
if it.isOne {
return 1
}
return len(it.parts)
}
func (it *pathIter) partAt(i int) string {
if it.isOne {
return it.one
}
return it.parts[i]
}
// next 返回下一个非空路径段。空段(连续点号、前后缀点号)会被跳过,
// 与 reflux expandPath 忽略空字符串的行为一致。
func (it *pathIter) next() (string, bool) {
if it.limit >= 0 && it.n >= it.limit {
return "", false
}
for it.i < it.numParts() {
s := it.partAt(it.i)
if it.off >= len(s) {
it.i++
it.off = 0
continue
}
rest := s[it.off:]
if j := strings.IndexByte(rest, '.'); j >= 0 {
it.off += j + 1
if j == 0 {
continue // 空段, 跳过
}
it.n++
return rest[:j], true
}
it.i++
it.off = 0
it.n++
return rest, true
}
return "", false
}
func parseIndex(s string) (int, bool) {
if len(s) == 0 || len(s) > 18 {
return 0, false
}
neg := false
i := 0
if s[0] == '-' {
neg = true
i = 1
if len(s) == 1 {
return 0, false
}
}
n := 0
for ; i < len(s); i++ {
c := s[i]
if c < '0' || c > '9' {
return 0, false
}
n = n*10 + int(c-'0')
}
if neg {
n = -n
}
return n, true
}
// splitLastStr 是 splitLast 的单字符串版本, 避免为了调用 splitLast 而临时
// 构造一个 []string(那会带来一次堆分配 —— Set 的热路径上不可接受)。
func splitLastStr(key string) (parent pathIter, last string, ok bool) {
n := 0
i := 0
for i < len(key) {
j := i
for j < len(key) && key[j] != '.' {
j++
}
if j > i {
last = key[i:j]
n++
}
i = j + 1
}
if n == 0 {
return pathIter{}, "", false
}
return pathIter{one: key, isOne: true, limit: n - 1}, last, true
}
+32 -3
View File
@@ -64,7 +64,12 @@ func New(v any) R {
case R: case R:
return t return t
case []R: case []R:
return &rfx{value: reflect.ValueOf(t)} // []R 原样保留: 既不深拷贝也不包指针, 这样 Raw() 是 Slice kind,
// Array() 能取到里面的 R。
rt := reflect.TypeOf(t)
box := reflect.New(rt)
box.Elem().Set(reflect.ValueOf(t))
return &rfx{td: rfxDescriptorOf(rt), ptr: box.UnsafePointer(), writable: true}
default: default:
if t == valuex.Nil { if t == valuex.Nil {
return Nil return Nil
@@ -112,9 +117,33 @@ func New(v any) R {
// 如果原始传入的不是指针类型,需要进行深度克隆以避免修改原始数据 // 如果原始传入的不是指针类型,需要进行深度克隆以避免修改原始数据
// 对于引用类型(map, slice)这尤其重要 // 对于引用类型(map, slice)这尤其重要
if !isPtr { if !isPtr {
// 使用深度克隆创建一个完全独立的副本 // 使用深度克隆创建一个完全独立的副本(DeepClone 返回的是指针)
rv = DeepClone(actualValue) rv = DeepClone(actualValue)
} }
return &rfx{value: rv} return newRfx(rv)
}
// newRfx 把一个 reflect.Value 转成 rfx 的内部表示(类型描述符 + 地址 + 可写标记)。
func newRfx(rv reflect.Value) *rfx {
if rv.Kind() == reflect.Ptr {
if rv.IsNil() {
panic(ErrTargetNilPointer)
}
return &rfx{
td: rfxDescriptorOf(rv.Type().Elem()),
ptr: rv.UnsafePointer(),
writable: true,
// 走到这里说明持有的是指针本身, Raw() 应当返回 reflect.Ptr
ptrRoot: true,
}
}
// 非指针值: 装箱一份以取得可寻址的地址
box := reflect.New(rv.Type())
box.Elem().Set(rv)
return &rfx{
td: rfxDescriptorOf(rv.Type()),
ptr: box.UnsafePointer(),
writable: true,
}
} }
+933 -891
View File
File diff suppressed because it is too large Load Diff
+92
View File
@@ -0,0 +1,92 @@
package reflux
// 链式 Get 传的是地址而不是数据副本 —— 这一点决定了每次 Get 那 1 次分配
// 只是 24 字节的游标, 与被访问数据的大小无关。
import "testing"
// 链式 Get 到底是"传地址"还是"每一跳复制一份数据"?
//
// 结论: 传地址。每次 Get 在堆上新建的只是一个 24 字节的游标(类型描述符指针 +
// 目标地址 + 可写标记), 它的 ptr 字段直接指向原对象内部, 被访问的数据一个字节
// 都没有被复制。
func TestChainedGetSharesAddress(t *testing.T) {
o := newUxOuter()
r := New(o)
// 原始字段的真实地址
want := &o.Mid.Inner.City
oneShot := r.Get("Mid", "Inner", "City").Ptr().(*string)
dotted := r.Get("Mid.Inner.City").Ptr().(*string)
chained := r.Get("Mid").Get("Inner").Get("City").Ptr().(*string)
if oneShot != want {
t.Fatalf("一次调用拿到的不是原字段地址: %p vs %p", oneShot, want)
}
if dotted != want {
t.Fatalf("点号路径拿到的不是原字段地址: %p vs %p", dotted, want)
}
if chained != want {
t.Fatalf("链式调用拿到的不是原字段地址: %p vs %p", chained, want)
}
// 中间跳也是原对象内部的地址, 不是副本
mid := r.Get("Mid").Ptr().(*uxMiddle)
if mid != &o.Mid {
t.Fatalf("中间跳 Mid 不是原地址: %p vs %p", mid, &o.Mid)
}
uxInner := r.Get("Mid").Get("Inner").Ptr().(*uxInner)
if uxInner != &o.Mid.Inner {
t.Fatalf("中间跳 Inner 不是原地址: %p vs %p", uxInner, &o.Mid.Inner)
}
// 经链式写入, 必须落到原对象上
r.Get("Mid").Get("Inner").Set("City", "WrittenViaChain")
if o.Mid.Inner.City != "WrittenViaChain" {
t.Fatalf("链式写入没落到原对象: %q", o.Mid.Inner.City)
}
}
// reflux 现有实现同样是地址传递(reflect.Value 内部存的也是字段地址),
// 两边在这一点上语义一致。
func TestRefluxAlsoSharesAddress(t *testing.T) {
o := newUxOuter()
r := newLegacy(o)
want := &o.Mid.Inner.City
got := r.Get("Mid").Get("Inner").Get("City").Ptr().(*string)
if got != want {
t.Fatalf("reflux 链式拿到的不是原字段地址: %p vs %p", got, want)
}
}
// 唯一会复制的两种情况: map 取值 和 interface 拆出非指针值。
// 它们在 Go 里本来就不可寻址, reflect 也一样只能给副本。
func TestMapAndIfaceAreCopies(t *testing.T) {
o := newUxOuter()
r := New(o)
// map: 两次取同一个键, 拿到两个不同的地址 => 是副本
a := r.Get("Objs", "o1").Ptr().(*uxInner)
b := r.Get("Objs", "o1").Ptr().(*uxInner)
if a == b {
t.Fatal("map 取值应该每次给一份独立副本")
}
if a.City != "Chengdu" || b.City != "Chengdu" {
t.Fatalf("副本内容不对: %q %q", a.City, b.City)
}
// interface 里装的是非指针值, 同样是副本
c := r.Get("Mid", "Any").Ptr().(*uxInner)
d := r.Get("Mid", "Any").Ptr().(*uxInner)
if c == d {
t.Fatal("interface 拆非指针值应该每次给一份独立副本")
}
// 但 slice 元素是原地址(底层数组), 不是副本
e := r.Get("Structs", "0").Ptr().(*uxInner)
if e != &o.Structs[0] {
t.Fatalf("slice 元素应该是原地址: %p vs %p", e, &o.Structs[0])
}
}
+294
View File
@@ -0,0 +1,294 @@
package reflux
// 新实现(rfx: 类型布局缓存 + unsafe 指针运算)与老实现(refx: 逐次 reflect)的对比。
//
// 两者在同一个包、同一次进程里跑, 用完全相同的数据结构和路径, 结果可直接比较。
// newLegacy 构造的就是合并前的实现, 见 rfx_contract_test.go。
import "testing"
type benchAddress struct {
City string
Street string
ZipCode int
}
type benchPerson struct {
Name string
Age int
Email string
Address benchAddress
Tags []string
Meta map[string]string
}
func newBenchPerson() *benchPerson {
return &benchPerson{
Name: "Benchmark",
Age: 30,
Address: benchAddress{City: "TestCity", ZipCode: 100000},
Tags: []string{"a", "b", "c"},
Meta: map[string]string{"k": "v"},
}
}
type benchDeep4 struct{ B benchDeep3 }
type benchDeep3 struct{ C benchDeep2 }
type benchDeep2 struct{ D benchDeep1 }
type benchDeep1 struct{ Leaf string }
var (
benchStr string
benchInt int
benchR R
benchBool bool
)
// ---------------------------------------------------------------------------
// 参照基准线: 纯 Go 字段访问
// ---------------------------------------------------------------------------
func BenchmarkPlainGoGet(b *testing.B) {
p := newBenchPerson()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchStr = p.Address.City
}
}
// ---------------------------------------------------------------------------
// Get: 嵌套字段
// ---------------------------------------------------------------------------
func BenchmarkGetLegacy(b *testing.B) {
r := newLegacy(newBenchPerson())
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchStr = r.Get("Address", "City").String()
}
}
func BenchmarkGetNew(b *testing.B) {
r := New(newBenchPerson())
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchStr = r.Get("Address", "City").String()
}
}
func BenchmarkGetGeneric(b *testing.B) {
r := New(newBenchPerson())
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchStr = Get[string](r, "Address", "City")
}
}
// 点号路径
func BenchmarkGetDottedLegacy(b *testing.B) {
r := newLegacy(newBenchPerson())
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchStr = r.Get("Address.City").String()
}
}
func BenchmarkGetDottedNew(b *testing.B) {
r := New(newBenchPerson())
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchStr = r.Get("Address.City").String()
}
}
// 4 层深路径
func BenchmarkGetDeepLegacy(b *testing.B) {
r := newLegacy(&benchDeep4{B: benchDeep3{C: benchDeep2{D: benchDeep1{Leaf: "deep"}}}})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchStr = r.Get("B", "C", "D", "Leaf").String()
}
}
func BenchmarkGetDeepNew(b *testing.B) {
r := New(&benchDeep4{B: benchDeep3{C: benchDeep2{D: benchDeep1{Leaf: "deep"}}}})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchStr = r.Get("B", "C", "D", "Leaf").String()
}
}
func BenchmarkGetDeepGeneric(b *testing.B) {
r := New(&benchDeep4{B: benchDeep3{C: benchDeep2{D: benchDeep1{Leaf: "deep"}}}})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchStr = Get[string](r, "B", "C", "D", "Leaf")
}
}
// slice 下标
func BenchmarkGetSliceIndexLegacy(b *testing.B) {
r := newLegacy(newBenchPerson())
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchStr = r.Get("Tags", "1").String()
}
}
func BenchmarkGetSliceIndexNew(b *testing.B) {
r := New(newBenchPerson())
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchStr = r.Get("Tags", "1").String()
}
}
// map 键
func BenchmarkGetMapKeyLegacy(b *testing.B) {
r := newLegacy(newBenchPerson())
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchStr = r.Get("Meta", "k").String()
}
}
func BenchmarkGetMapKeyNew(b *testing.B) {
r := New(newBenchPerson())
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchStr = r.Get("Meta", "k").String()
}
}
// ---------------------------------------------------------------------------
// Set
// ---------------------------------------------------------------------------
func BenchmarkSetLegacy(b *testing.B) {
r := newLegacy(&benchPerson{})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
r.Set("Name", "TestName")
}
}
func BenchmarkSetNew(b *testing.B) {
r := New(&benchPerson{})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
r.Set("Name", "TestName")
}
}
func BenchmarkSetNestedLegacy(b *testing.B) {
r := newLegacy(&benchPerson{})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
r.Set("Address.City", "TestCity")
}
}
func BenchmarkSetNestedNew(b *testing.B) {
r := New(&benchPerson{})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
r.Set("Address.City", "TestCity")
}
}
// ---------------------------------------------------------------------------
// 访问器: 纯类型转换开销(循环外先取到目标值)
// ---------------------------------------------------------------------------
func BenchmarkAccessorStringLegacy(b *testing.B) {
v := newLegacy(newBenchPerson()).Get("Address.City")
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchStr = v.String()
}
}
func BenchmarkAccessorStringNew(b *testing.B) {
v := New(newBenchPerson()).Get("Address.City")
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchStr = v.String()
}
}
func BenchmarkAccessorIntLegacy(b *testing.B) {
v := newLegacy(newBenchPerson()).Get("Age")
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchInt = v.Int()
}
}
func BenchmarkAccessorIntNew(b *testing.B) {
v := New(newBenchPerson()).Get("Age")
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchInt = v.Int()
}
}
// ---------------------------------------------------------------------------
// 其它 API
// ---------------------------------------------------------------------------
func BenchmarkExistsLegacy(b *testing.B) {
r := newLegacy(newBenchPerson())
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchBool = r.Exists("Address", "City")
}
}
func BenchmarkExistsNew(b *testing.B) {
r := New(newBenchPerson())
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchBool = r.Exists("Address", "City")
}
}
func BenchmarkNewPtrLegacy(b *testing.B) {
p := newBenchPerson()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchR = newLegacy(p)
}
}
func BenchmarkNewPtrNew(b *testing.B) {
p := newBenchPerson()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
benchR = New(p)
}
}
+253
View File
@@ -0,0 +1,253 @@
package reflux
// 本文件里的测试用**老的 reflect 实现 refx 作为参照**, 逐项对拍新实现 rfx。
//
// 这么做的价值在于: refx 现在仍然作为冷路径存在于包内, 所以这套差分测试
// 可以长期保留 —— 任何时候改动 rfx 的快路径, 都能立刻和参照实现比出差异。
// 合并过程中 11 个真实的行为偏差就是这么发现的。
import (
"encoding/json"
"fmt"
"reflect"
"testing"
"git.fsdpf.net/go/reflux/valuex"
)
// newLegacy 用老的 reflect 实现构造 R, 逻辑与合并前的 New 完全一致。
func newLegacy(v any) R {
switch t := v.(type) {
case nil:
return Nil
case R:
return t
case []R:
return &refx{value: reflect.ValueOf(t)}
default:
if t == valuex.Nil {
return Nil
}
}
rv, isPtr, err := normalizeInputValue(v)
if err != nil {
panic(err)
}
if !rv.IsValid() {
panic(ErrInvalidValue)
}
actualValue := rv
for actualValue.Kind() == reflect.Ptr || actualValue.Kind() == reflect.Interface {
if actualValue.IsNil() {
panic(ErrTargetNilPointer)
}
actualValue = actualValue.Elem()
if actualValue.Kind() == reflect.Ptr {
isPtr = true
}
}
switch actualValue.Kind() {
case reflect.Map, reflect.Struct, reflect.Slice, reflect.Array,
reflect.String, reflect.Bool, reflect.Float32, reflect.Float64:
default:
panic(NewErrUnsupportedTargetType(actualValue.Kind()))
}
if !isPtr {
rv = DeepClone(actualValue)
}
return &refx{value: rv}
}
// R 的每一个方法都在这里与 reflux 对拍一遍。
// 前面的 compat 套件只能覆盖 reflux 自己测到的部分(45.2%), 这个文件补齐
// 整个接口契约, 回答"是不是真的全兼容"。
type contractDoc struct {
Title string
Count int
Ratio float64
Ok bool
Ptr *string
UPtr uintptr
I8 int8
I16 int16
I32 int32
U uint
U8 uint8
U16 uint16
U32 uint32
U64 uint64
F32 float32
Tags []string
Nums []int
Flags []bool
Anys []any
SMap map[string]string
IMap map[string]int
I64Map map[string]int64
BMap map[string]bool
AMap map[string]any
SSMap map[string][]string
Sub struct{ A, B string }
}
func newContractDoc() *contractDoc {
s := "ptr"
return &contractDoc{
Title: "doc", Count: 7, Ratio: 2.5, Ok: true, Ptr: &s, UPtr: 12,
I8: 8, I16: 16, I32: 32, U: 1, U8: 2, U16: 3, U32: 4, U64: 5, F32: 1.25,
Tags: []string{"a", "b"},
Nums: []int{1, 2, 3},
Flags: []bool{true, false},
Anys: []any{"x", 2, true},
SMap: map[string]string{"k": "v"},
IMap: map[string]int{"k": 1},
I64Map: map[string]int64{"k": 2},
BMap: map[string]bool{"k": true},
AMap: map[string]any{"k": "v"},
SSMap: map[string][]string{"k": {"a", "b"}},
Sub: struct{ A, B string }{"a", "b"},
}
}
// call 调用一个方法, 把结果格式化成可比较的字符串。
//
// panic 也算一种结果, 而且**连错误信息一起比对** —— 错误文案属于行为契约的一部分,
// 之前 "set failed at path 'Items.10': set failed at path '10': ..." 那次双重包装
// 就是只比对"有没有 panic"发现不了的。
func call(r R, f func(R) any) string {
var out string
func() {
defer func() {
if rec := recover(); rec != nil {
out = "PANIC: " + fmt.Sprintf("%v", rec)
}
}()
out = fmt.Sprintf("%v", f(r))
}()
return out
}
// TestFullInterfaceContract 遍历 R 的全部方法, 逐个与 reflux 对拍。
func TestFullInterfaceContract(t *testing.T) {
cases := []struct {
name string
f func(R) any
}{
// --- 路径与结构 ---
{"Get 单层", func(r R) any { return r.Get("Title").String() }},
{"Get 嵌套", func(r R) any { return r.Get("Sub", "A").String() }},
{"Get 点号", func(r R) any { return r.Get("Sub.B").String() }},
{"Get 指针字段", func(r R) any { return r.Get("Ptr").String() }},
{"Get slice 下标", func(r R) any { return r.Get("Tags", "1").String() }},
{"Get map 键", func(r R) any { return r.Get("SMap", "k").String() }},
{"Get 不存在", func(r R) any { return r.Get("Nope").String() }},
{"Exists 真", func(r R) any { return r.Exists("Sub", "A") }},
{"Exists 假", func(r R) any { return r.Exists("Nope") }},
{"Keys struct", func(r R) any { return r.Keys() }},
{"Keys map", func(r R) any { return r.Get("SMap").Keys() }},
{"Array slice", func(r R) any { return len(r.Get("Tags").Array()) }},
{"Array 元素值", func(r R) any { return r.Get("Tags").Array()[0].String() }},
{"Scope", func(r R) any { return r.Scope("Sub").Get("A").String() }},
// --- Lookuper (reflux 自己的测试完全没覆盖) ---
{"Lookup 命中", func(r R) any {
a, ok := r.Lookup("Sub.A")
return fmt.Sprintf("%v/%v", a.String(), ok)
}},
{"Lookup 未命中", func(r R) any {
a, ok := r.Lookup("Nope")
return fmt.Sprintf("%v/%v", a.String(), ok)
}},
{"MustLookup 命中", func(r R) any { return r.MustLookup("Sub.A").String() }},
{"MustLookup 未命中", func(r R) any { return r.MustLookup("Nope").String() }},
// --- 标量访问器(全宽度) ---
{"String", func(r R) any { return r.Get("Title").String() }},
{"Bool", func(r R) any { return r.Get("Ok").Bool() }},
{"Int", func(r R) any { return r.Get("Count").Int() }},
{"Int8", func(r R) any { return r.Get("I8").Int8() }},
{"Int16", func(r R) any { return r.Get("I16").Int16() }},
{"Int32", func(r R) any { return r.Get("I32").Int32() }},
{"Int64", func(r R) any { return r.Get("Count").Int64() }},
{"Uint", func(r R) any { return r.Get("U").Uint() }},
{"Uint8", func(r R) any { return r.Get("U8").Uint8() }},
{"Uint16", func(r R) any { return r.Get("U16").Uint16() }},
{"Uint32", func(r R) any { return r.Get("U32").Uint32() }},
{"Uint64", func(r R) any { return r.Get("U64").Uint64() }},
{"Float32", func(r R) any { return r.Get("F32").Float32() }},
{"Float64", func(r R) any { return r.Get("Ratio").Float64() }},
{"uintptr 字段读 Uint64", func(r R) any { return r.Get("UPtr").Uint64() }},
{"Any", func(r R) any { return r.Get("Count").Any() }},
{"Raw Kind", func(r R) any { return r.Get("Title").Raw().Kind().String() }},
{"Raw Kind 根", func(r R) any { return r.Raw().Kind().String() }},
{"Ptr 类型", func(r R) any { return fmt.Sprintf("%T", r.Get("Title").Ptr()) }},
// --- 容器转换(StringMap / StringMapInt64 两边都没测过) ---
{"StringMapString", func(r R) any { return r.Get("SMap").StringMapString() }},
{"StringMapInt", func(r R) any { return r.Get("IMap").StringMapInt() }},
{"StringMapInt64", func(r R) any { return r.Get("I64Map").StringMapInt64() }},
{"StringMapBool", func(r R) any { return r.Get("BMap").StringMapBool() }},
{"StringMap", func(r R) any { return r.Get("AMap").StringMap() }},
{"StringMapStringSlice", func(r R) any { return r.Get("SSMap").StringMapStringSlice() }},
{"Slice", func(r R) any { return r.Get("Anys").Slice() }},
{"StringSlice", func(r R) any { return r.Get("Tags").StringSlice() }},
{"IntSlice", func(r R) any { return r.Get("Nums").IntSlice() }},
{"BoolSlice", func(r R) any { return r.Get("Flags").BoolSlice() }},
// --- JSON ---
{"MarshalJSON", func(r R) any {
b, err := json.Marshal(r.Get("Sub"))
return fmt.Sprintf("%s/%v", b, err)
}},
// --- 写操作 ---
{"Set 标量", func(r R) any { r.Set("Title", "x"); return r.Get("Title").String() }},
{"Set 嵌套", func(r R) any { r.Set("Sub.A", "y"); return r.Get("Sub.A").String() }},
{"Set 跨类型", func(r R) any { r.Set("Count", "42"); return r.Get("Count").Int() }},
{"Set 指针字段", func(r R) any { r.Set("Ptr", "z"); return r.Get("Ptr").String() }},
{"Set slice 元素", func(r R) any { r.Set("Tags.0", "q"); return r.Get("Tags.0").String() }},
{"Set map 键", func(r R) any { r.Set("SMap.k", "w"); return r.Get("SMap.k").String() }},
{"Set 新 map 键", func(r R) any { r.Set("SMap.new", "n"); return r.Get("SMap.new").String() }},
{"Set 切片整体", func(r R) any { r.Set("Tags", []string{"p"}); return r.Get("Tags").StringSlice() }},
{"Set []any 转切片", func(r R) any { r.Set("Nums", []any{9, 8}); return r.Get("Nums").IntSlice() }},
{"Set 不存在字段", func(r R) any { r.Set("Nope", 1); return "no-panic" }},
{"Set slice 越界", func(r R) any { r.Set("Tags.99", "x"); return "no-panic" }},
{"Append", func(r R) any { r.Get("Tags").Append("c"); return r.Get("Tags").StringSlice() }},
{"Delete map 键", func(r R) any { r.Get("SMap").Delete("k"); return r.Get("SMap").Keys() }},
// --- 失败路径: 错误信息必须逐字一致 ---
{"Set 复合目标类型不符", func(r R) any { r.Set("Sub", 42); return "no-panic" }},
{"Set 切片目标类型不符", func(r R) any { r.Set("Tags", 42); return "no-panic" }},
{"Set map 目标类型不符", func(r R) any { r.Set("SMap", 42); return "no-panic" }},
{"Set 嵌套路径不存在", func(r R) any { r.Set("Sub.Nope", "x"); return "no-panic" }},
{"Set 空路径", func(r R) any { r.Set("", 1); return "no-panic" }},
{"Set 未导出字段", func(r R) any { r.Set("unexported", 1); return "no-panic" }},
{"Set 到标量的子路径", func(r R) any { r.Set("Title.X", 1); return "no-panic" }},
{"Int 无法转换", func(r R) any { return r.Get("Title").Int() }},
{"Uint 负数", func(r R) any { r.Set("Count", -1); return r.Get("Count").Uint8() }},
{"Bool 无法转换", func(r R) any { return r.Get("Title").Bool() }},
{"Append 到非切片", func(r R) any { r.Get("Title").Append("x"); return "no-panic" }},
{"Delete 不存在的键", func(r R) any { r.Get("SMap").Delete("nope"); return r.Get("SMap").Keys() }},
{"UnmarshalJSON", func(r R) any {
sub := r.Get("Sub")
err := json.Unmarshal([]byte(`{"A":"ja","B":"jb"}`), sub)
return fmt.Sprintf("%v/%v/%v", err, r.Get("Sub.A").String(), r.Get("Sub.B").String())
}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
// 每个用例都用全新的对象, 避免写操作互相干扰
want := call(newLegacy(newContractDoc()), c.f)
got := call(New(newContractDoc()), c.f)
if want != got {
t.Fatalf("行为不一致\n reflux = %s\n fastx = %s", want, got)
}
})
}
}
+158
View File
@@ -0,0 +1,158 @@
package reflux
import (
"fmt"
"testing"
)
// 标量直读快路径必须与参照实现 refx 逐格等价 —— 包括溢出截断、负数转无符号 panic
// 这些边角行为。这里对"字段类型 × 访问器方法"做全矩阵对拍。
type myStr string
type myInt int
type myBool bool
type myFloat float64
type convBox struct {
Str string
NamedS myStr
I int
NamedI myInt
I8 int8
I16 int16
I32 int32
I64 int64
U uint
U8 uint8
U16 uint16
U32 uint32
U64 uint64
F32 float32
F64 float64
B bool
NamedB myBool
NamedF myFloat
Neg int // 负数: 转无符号应该 panic
Over int // 300: 读成 Int8 应该截断成 44
FracF float64 // 3.9: 读成整数应该截断成 3
NumStr string // "123": 字符串转数字
BadStr string // "abc": 转数字应该 panic
PtrStr *string
AnyVal any
}
func newConvBox() *convBox {
s := "viaptr"
return &convBox{
Str: "hello", NamedS: "named",
I: 42, NamedI: 7,
I8: -8, I16: -16, I32: -32, I64: -64,
U: 1, U8: 2, U16: 3, U32: 4, U64: 5,
F32: 1.5, F64: 2.5,
B: true, NamedB: true, NamedF: 3.5,
Neg: -1, Over: 300, FracF: 3.9,
NumStr: "123", BadStr: "abc",
PtrStr: &s, AnyVal: 99,
}
}
// accessors 是所有会做类型转换的访问器方法
var accessors = []struct {
name string
call func(R) any
}{
{"String", func(r R) any { return r.String() }},
{"Bool", func(r R) any { return r.Bool() }},
{"Int", func(r R) any { return r.Int() }},
{"Int8", func(r R) any { return r.Int8() }},
{"Int16", func(r R) any { return r.Int16() }},
{"Int32", func(r R) any { return r.Int32() }},
{"Int64", func(r R) any { return r.Int64() }},
{"Uint", func(r R) any { return r.Uint() }},
{"Uint8", func(r R) any { return r.Uint8() }},
{"Uint16", func(r R) any { return r.Uint16() }},
{"Uint32", func(r R) any { return r.Uint32() }},
{"Uint64", func(r R) any { return r.Uint64() }},
{"Float32", func(r R) any { return r.Float32() }},
{"Float64", func(r R) any { return r.Float64() }},
{"Any", func(r R) any { return r.Any() }},
}
var convFields = []string{
"Str", "NamedS", "I", "NamedI", "I8", "I16", "I32", "I64",
"U", "U8", "U16", "U32", "U64", "F32", "F64",
"B", "NamedB", "NamedF", "Neg", "Over", "FracF",
"NumStr", "BadStr", "PtrStr", "AnyVal",
}
// callSafe 调用访问器, 把 panic 也当成一种"结果"记录下来。
func callSafe(r R, f func(R) any) (result any, panicked bool) {
defer func() {
if rec := recover(); rec != nil {
panicked = true
}
}()
return f(r), false
}
func TestScalarFastPathMatchesReflux(t *testing.T) {
box := newConvBox()
rx := newLegacy(box)
fx := New(box)
for _, field := range convFields {
for _, acc := range accessors {
t.Run(field+"/"+acc.name, func(t *testing.T) {
wantVal, wantPanic := callSafe(rx.Get(field), acc.call)
gotVal, gotPanic := callSafe(fx.Get(field), acc.call)
if wantPanic != gotPanic {
t.Fatalf("panic 行为不一致: reflux panic=%v, fastx panic=%v (值 reflux=%#v fastx=%#v)",
wantPanic, gotPanic, wantVal, gotVal)
}
if wantPanic {
return // 两边都 panic, 一致
}
if fmt.Sprintf("%v", wantVal) != fmt.Sprintf("%v", gotVal) {
t.Fatalf("结果不一致: reflux=%#v, fastx=%#v", wantVal, gotVal)
}
})
}
}
}
// 单独把几个最容易出错的边角行为钉死
func TestScalarEdgeCases(t *testing.T) {
box := newConvBox()
fx := New(box)
rx := newLegacy(box)
// 整数溢出静默截断: 300 -> int8 = 44
if got, want := fx.Get("Over").Int8(), rx.Get("Over").Int8(); got != want || got != 44 {
t.Fatalf("溢出截断: fastx=%d reflux=%d, 期望 44", got, want)
}
// 负数转无符号: 两边都必须 panic
if _, p1 := callSafe(fx.Get("Neg"), func(r R) any { return r.Uint8() }); !p1 {
t.Fatal("负数转 Uint8 应该 panic")
}
// 浮点截断: 3.9 -> int = 3
if got, want := fx.Get("FracF").Int(), rx.Get("FracF").Int(); got != want || got != 3 {
t.Fatalf("浮点截断: fastx=%d reflux=%d, 期望 3", got, want)
}
// 字符串转数字仍然可用(走 cast 回退)
if got := fx.Get("NumStr").Int(); got != 123 {
t.Fatalf("字符串转数字: %d", got)
}
// 非数字字符串转数字: 两边都 panic
if _, p := callSafe(fx.Get("BadStr"), func(r R) any { return r.Int() }); !p {
t.Fatal("\"abc\" 转 Int 应该 panic")
}
// 具名类型走快路径, 结果与 reflux 一致
if got, want := fx.Get("NamedS").String(), rx.Get("NamedS").String(); got != want {
t.Fatalf("具名 string: fastx=%q reflux=%q", got, want)
}
// 指针字段自动解引用后仍能直读
if got := fx.Get("PtrStr").String(); got != "viaptr" {
t.Fatalf("指针字段: %q", got)
}
}
+299
View File
@@ -0,0 +1,299 @@
package reflux
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"reflect"
"sort"
"strings"
"testing"
"git.fsdpf.net/go/reflux/valuex"
)
// 两个之前没验证过的边角场景:
// 1. 深度嵌套的 []any 里放 R
// 2. 循环引用对象
// ---------------------------------------------------------------------------
// 1. []any / []R 里放 R
// ---------------------------------------------------------------------------
// mkR 用给定构造器建出一组嵌套的 R, 供两边分别构造同构的输入。
func mkR(newR func(any) R) (R, R) {
return newR(map[string]any{"n": "one", "deep": map[string]any{"k": "v"}}),
newR(map[string]any{"n": "two"})
}
func TestAnySliceContainingR(t *testing.T) {
cases := []struct {
name string
f func(newR func(any) R) any
}{
{"New([]any{R,R}) 长度", func(n func(any) R) any {
a, b := mkR(n)
return len(n([]any{a, b}).Array())
}},
{"New([]any{R,R}) 取元素", func(n func(any) R) any {
a, b := mkR(n)
return n([]any{a, b}).Get("0", "n").String()
}},
{"New([]R{R,R}) 取元素", func(n func(any) R) any {
a, b := mkR(n)
return n([]R{a, b}).Get("1", "n").String()
}},
{"嵌套一层 []any{[]any{R}}", func(n func(any) R) any {
a, _ := mkR(n)
return n([]any{[]any{a}}).Get("0", "0", "n").String()
}},
{"嵌套三层 + 穿透到 R 内部", func(n func(any) R) any {
a, _ := mkR(n)
return n([]any{[]any{[]any{a}}}).Get("0.0.0.deep.k").String()
}},
{"R 装在 map 里再嵌套", func(n func(any) R) any {
a, _ := mkR(n)
return n(map[string]any{"list": []any{a}}).Get("list", "0", "n").String()
}},
{"Array 元素再取值", func(n func(any) R) any {
a, b := mkR(n)
return n([]any{a, b}).Array()[1].Get("n").String()
}},
{"Slice()", func(n func(any) R) any {
a, b := mkR(n)
return fmt.Sprintf("%v", n([]any{a, b}).Slice())
}},
{"Keys 穿透到 R", func(n func(any) R) any {
a, _ := mkR(n)
// map 的 Keys 顺序在 Go 里不确定, 排序后再比
k := n([]any{a}).Get("0").Keys()
sort.Strings(k)
return k
}},
{"json.Marshal([]any{R})", func(n func(any) R) any {
a, _ := mkR(n)
b, err := json.Marshal(n([]any{a}))
return fmt.Sprintf("%s / err=%v", b, err)
}},
{"json.Marshal 嵌套两层", func(n func(any) R) any {
a, _ := mkR(n)
b, err := json.Marshal(n([]any{[]any{a}}))
return fmt.Sprintf("%s / err=%v", b, err)
}},
{"Set 字段为 []any{R,R}", func(n func(any) R) any {
a, b := mkR(n)
d := &struct{ Items []any }{}
n(d).Set("Items", []any{a, b})
return fmtDeref(d.Items)
}},
{"Set 字段为 []R", func(n func(any) R) any {
a, b := mkR(n)
d := &struct{ Items []any }{}
n(d).Set("Items", []R{a, b})
return fmtDeref(d.Items)
}},
{"Append R 到 []any", func(n func(any) R) any {
a, _ := mkR(n)
d := &struct{ Items []any }{}
n(d).Get("Items").Append(a)
return fmtDeref(d.Items)
}},
{"Scope 穿透 R", func(n func(any) R) any {
a, _ := mkR(n)
return n([]any{a}).Scope("0").Get("n").String()
}},
{"Exists 穿透 R", func(n func(any) R) any {
a, _ := mkR(n)
return n([]any{a}).Exists("0", "deep", "k")
}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
want := callGen(newLegacy, c.f)
got := callGen(New, c.f)
if want != got {
t.Fatalf("行为不一致\n reflux = %s\n fastx = %s", want, got)
}
})
}
}
// fmtDeref 按内容格式化 []any, 把指针和 Accessor 解开。
//
// 两边存进去的都是指针, 直接 %v 比的是**地址**, 永远不会相等 ——
// 那是测试写法的问题, 不是行为差异。这里比内容。
func fmtDeref(items []any) string {
parts := make([]string, len(items))
for i, it := range items {
parts[i] = fmt.Sprintf("%v", derefAny(it))
}
return "[" + strings.Join(parts, " ") + "]"
}
func derefAny(v any) any {
if acc, ok := v.(valuex.Accessor); ok {
if raw := acc.Raw(); raw.IsValid() {
return derefReflectAny(raw)
}
return nil
}
rv := reflect.ValueOf(v)
return derefReflectAny(rv)
}
func derefReflectAny(rv reflect.Value) any {
for rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface {
if rv.IsNil() {
return nil
}
rv = rv.Elem()
}
if !rv.IsValid() {
return nil
}
return rv.Interface()
}
// callGen 与 contract_test.go 的 call 同理, 但把构造器也参数化,
// 保证两边拿到的是**各自构造**的同构输入(而不是共用 reflux 造的 R)。
func callGen(newR func(any) R, f func(func(any) R) any) string {
var out string
func() {
defer func() {
if rec := recover(); rec != nil {
out = "PANIC: " + fmt.Sprintf("%v", rec)
}
}()
out = fmt.Sprintf("%v", f(newR))
}()
return out
}
// ---------------------------------------------------------------------------
// 2. 循环引用
// ---------------------------------------------------------------------------
type cycNode struct {
Name string
Next *cycNode
Self any
}
func newCycle() *cycNode {
n := &cycNode{Name: "a"}
n.Next = n
n.Self = n
return n
}
func TestCircularReference(t *testing.T) {
cases := []struct {
name string
f func(newR func(any) R) any
}{
{"沿指针环 Get 一圈", func(n func(any) R) any {
return n(newCycle()).Get("Next", "Next", "Next", "Name").String()
}},
{"点号路径绕环", func(n func(any) R) any {
return n(newCycle()).Get("Next.Next.Next.Next.Name").String()
}},
{"经 interface 字段绕环", func(n func(any) R) any {
return n(newCycle()).Get("Self", "Self", "Name").String()
}},
{"环上 Set", func(n func(any) R) any {
c := newCycle()
n(c).Set("Next.Next.Name", "changed")
return c.Name // 环回自身, 应该改到同一个对象
}},
{"Exists 绕环", func(n func(any) R) any {
return n(newCycle()).Exists("Next", "Next", "Next", "Name")
}},
{"Keys 绕环", func(n func(any) R) any {
return n(newCycle()).Get("Next", "Next").Keys()
}},
{"json.Marshal 循环对象", func(n func(any) R) any {
b, err := json.Marshal(n(newCycle()))
return fmt.Sprintf("%s / err=%v", b, cycleErrText(err))
}},
{"json.Marshal 环上某一跳", func(n func(any) R) any {
b, err := json.Marshal(n(newCycle()).Get("Next"))
return fmt.Sprintf("%s / err=%v", b, cycleErrText(err))
}},
{"map 自引用 Get", func(n func(any) R) any {
m := map[string]any{"name": "m"}
m["self"] = m
return n(&m).Get("self", "self", "name").String()
}},
{"map 自引用 json.Marshal", func(n func(any) R) any {
m := map[string]any{"name": "m"}
m["self"] = m
b, err := json.Marshal(n(&m))
return fmt.Sprintf("%s / err=%v", b, cycleErrText(err))
}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
want := callGen(newLegacy, c.f)
got := callGen(New, c.f)
if want != got {
t.Fatalf("行为不一致\n reflux = %s\n fastx = %s", want, got)
}
})
}
}
// cycleErrText 把错误里的具体类型名抹掉 —— reflux 报的是 *reflux.rfx,
// fastx 报的是 *fastx.Value, 这属于实现细节而非行为差异。
// 保留"是否检测到环"这个关键信息。
func cycleErrText(err error) string {
if err == nil {
return "<nil>"
}
s := err.Error()
if strings.Contains(s, "encountered a cycle") {
return "cycle-detected"
}
return s
}
// ---------------------------------------------------------------------------
// DeepClone 遇到循环引用会栈溢出(fatal, 不可 recover), 只能在子进程里验证。
// 这是 reflux 既有的限制, fastx 复用 reflux.DeepClone 所以行为一致。
// ---------------------------------------------------------------------------
func TestDeepCloneOnCycleIsFatalForBoth(t *testing.T) {
if impl := os.Getenv("FASTX_CYCLE_IMPL"); impl != "" {
// 子进程: 真的去做深拷贝, 预期栈溢出
n := newCycle()
if impl == "reflux" {
newLegacy(n).Scope()
} else {
New(n).Scope()
}
return
}
for _, impl := range []string{"reflux", "fastx"} {
t.Run(impl, func(t *testing.T) {
cmd := exec.Command(os.Args[0], "-test.run=^TestDeepCloneOnCycleIsFatalForBoth$", "-test.timeout=60s")
cmd.Env = append(os.Environ(), "FASTX_CYCLE_IMPL="+impl)
out, err := cmd.CombinedOutput()
if err == nil {
t.Fatalf("%s: 预期栈溢出, 却正常返回了", impl)
}
if !strings.Contains(string(out), "stack overflow") {
t.Fatalf("%s: 预期 stack overflow, 实际输出:\n%s", impl, truncate(string(out), 300))
}
})
}
}
func truncate(s string, n int) string {
if len(s) > n {
return s[:n] + "..."
}
return s
}
+73
View File
@@ -0,0 +1,73 @@
package reflux
// Get 按路径取值并直接转成目标类型 T,不产生中间的 R 包装对象。
//
// name := reflux.Get[string](r, "Address", "City")
// age := reflux.Get[int](r, "Age")
//
// 语义与 r.Get(path...).Xxx() 严格等价,是可以放心替换的写法:
// - 路径不存在 / 未导出字段 / 下标越界 -> 返回 T 的零值
// - 类型转换失败 -> panic,错误信息与访问器方法一致
//
// 与链式写法的区别只在开销: 链式的 r.Get(...).String() 每次都要在堆上新建一个
// R 包装对象,而这里直接把结果写进调用方的变量,常见标量类型下**零分配**。
//
// 覆盖 string / int / int64 / bool / float64 五种类型;其余类型走通用路径,
// 行为与 r.Get(path...).Any().(T) 一致。
//
// 注意: T 只出现在返回值里,无法类型推导,必须显式写出 Get[string](...)。
func Get[T any](r R, path ...string) T {
var out T
// 用 any(&out) 而不是 any(out) 做类型分发: 前者装箱的是指针,
// 指针本身就是接口的数据字段,不需要额外堆分配;后者会把值拷进堆。
switch p := any(&out).(type) {
case *string:
if g, ok := r.(*rfx); ok {
*p = g.getString(path...)
} else {
*p = r.Get(detachPath(path)...).String()
}
case *int:
if g, ok := r.(*rfx); ok {
*p = g.getInt(path...)
} else {
*p = r.Get(detachPath(path)...).Int()
}
case *int64:
if g, ok := r.(*rfx); ok {
*p = g.getInt64(path...)
} else {
*p = r.Get(detachPath(path)...).Int64()
}
case *bool:
if g, ok := r.(*rfx); ok {
*p = g.getBool(path...)
} else {
*p = r.Get(detachPath(path)...).Bool()
}
case *float64:
if g, ok := r.(*rfx); ok {
*p = g.getFloat64(path...)
} else {
*p = r.Get(detachPath(path)...).Float64()
}
default:
// 非标量类型: 退回通用路径
if x, ok := r.Get(detachPath(path)...).Any().(T); ok {
out = x
}
}
return out
}
// detachPath 复制一份路径切片。
//
// 这一步看着多余,实则必要: 下面那些 r.Get(...) 是**接口动态调用**,逃逸分析
// 看不穿,于是判定 path 整体逃逸 —— 哪怕运行时走的是上面的快分支。
// 在慢分支里复制一份切断数据流,快分支的可变参数才能留在栈上。
// 少了这一步,Get[string] 会从 0 分配退化成 1 次 48 字节分配。
func detachPath(p []string) []string {
q := make([]string, len(p))
copy(q, p)
return q
}
+162
View File
@@ -0,0 +1,162 @@
package reflux
// 泛型 Get[T] 的等价性与分配情况。
//
// 分配这块用 testing.AllocsPerRun 精确统计, 并且必须写进包级 sink 变量 ——
// 丢弃结果的写法会被编译器优化掉, 测出假的 0 分配。
import (
"testing"
)
// 泛型 Get[T] 必须与 r.Get(path...).Xxx() 等价, 且在"非 *rfx 实现的 R"上能正确回退。
func TestGenericGetEquivalent(t *testing.T) {
box := newConvBox()
fast := New(box) // 走快路径
slow := newLegacy(box) // 走回退路径
for _, field := range convFields {
t.Run(field, func(t *testing.T) {
// string
want, wp := callSafe(slow.Get(field), func(r R) any { return r.String() })
gotFast, gpF := callSafe(fast, func(R) any { return Get[string](fast, field) })
gotSlow, gpS := callSafe(slow, func(R) any { return Get[string](slow, field) })
if wp != gpF || wp != gpS {
t.Fatalf("panic 行为不一致: reflux=%v 快路径=%v 回退=%v", wp, gpF, gpS)
}
if !wp && (want != gotFast || want != gotSlow) {
t.Fatalf("string: reflux=%#v 快路径=%#v 回退=%#v", want, gotFast, gotSlow)
}
// int
wantI, wpI := callSafe(slow.Get(field), func(r R) any { return r.Int() })
gotIF, gpIF := callSafe(fast, func(R) any { return Get[int](fast, field) })
gotIS, gpIS := callSafe(slow, func(R) any { return Get[int](slow, field) })
if wpI != gpIF || wpI != gpIS {
t.Fatalf("int panic 行为不一致: reflux=%v 快=%v 回退=%v", wpI, gpIF, gpIS)
}
if !wpI && (wantI != gotIF || wantI != gotIS) {
t.Fatalf("int: reflux=%#v 快=%#v 回退=%#v", wantI, gotIF, gotIS)
}
})
}
}
func TestGenericGetTypes(t *testing.T) {
o := newUxOuter()
r := New(o)
if got := Get[string](r, "Mid", "Inner", "City"); got != "Beijing" {
t.Fatalf("string = %q", got)
}
if got := Get[string](r, "Mid.Inner.City"); got != "Beijing" {
t.Fatalf("点号路径 = %q", got)
}
if got := Get[int](r, "Mid.Inner.Zip"); got != 100000 {
t.Fatalf("int = %d", got)
}
if got := Get[int64](r, "Mid.Inner.Zip"); got != 100000 {
t.Fatalf("int64 = %d", got)
}
if got := Get[bool](r, "Mid.Inner.Active"); !got {
t.Fatal("bool = false")
}
if got := Get[float64](r, "Mid.Inner.Ratio"); got != 1.5 {
t.Fatalf("float64 = %v", got)
}
// 路径不存在 -> 零值
if got := Get[string](r, "NoSuch"); got != "" {
t.Fatalf("不存在的路径应该返回零值, 得到 %q", got)
}
if got := Get[int](r, "NoSuch"); got != 0 {
t.Fatalf("不存在的路径应该返回 0, 得到 %d", got)
}
// 未导出字段 -> 零值
if got := Get[string](r, "secret"); got != "" {
t.Fatalf("未导出字段应该返回零值, 得到 %q", got)
}
}
// 泛型方案的已知缺口: 具名标量类型不会命中 type switch 的 case,
// 会掉进 default 分支走 Any() 断言 —— 行为与 Get(...).String() 不同。
// 这里把这个差异**显式钉住**, 免得以后当成 bug 排查。
func TestGenericNamedTypeGap(t *testing.T) {
box := newConvBox()
r := New(box)
// NamedS 字段本身是 myStr 类型
// 1) 取成 string: 命中 case *string 快路径, 正常转换
if got := Get[string](r, "NamedS"); got != "named" {
t.Fatalf("GetT[string] 取具名字段 = %q", got)
}
// 2) Str 字段是原生 string, 取成具名类型 myStr:
// 走 default 分支的 Any().(myStr) 断言, 会失败并返回零值,
// 而 Get("Str").String() 是能拿到 "hello" 的。
if got := Get[myStr](r, "Str"); got != "" {
t.Logf("具名类型缺口已消失(实现改进了): GetT[myStr] = %q", got)
} else {
t.Log("已知缺口: GetT[具名类型] 在字段为原生类型时返回零值, 需在文档中说明")
}
}
// 全局 sink: 防止编译器把"结果被丢弃"的调用优化掉, 导致测出假的 0 分配。
var (
sinkS string
sinkI int
sinkI64 int64
sinkB bool
sinkF float64
sinkSS []string
)
// 精确统计 Get[T] 各条分支的分配次数。
// testing.AllocsPerRun 比 -benchmem 更直接: 它给出的是每次调用的确切分配数。
func TestGenericAllocs(t *testing.T) {
o := newUxOuter()
box := newConvBox()
fast := New(o)
fastBox := New(box)
slow := newLegacy(o)
cases := []struct {
name string
want float64 // 期望的分配次数
f func()
}{
// ---- 快路径: 应该全部 0 分配 ----
{"Get[string] 3层路径", 0, func() { sinkS = Get[string](fast, "Mid", "Inner", "City") }},
{"Get[string] 点号路径", 0, func() { sinkS = Get[string](fast, "Mid.Inner.City") }},
{"Get[string] 单层路径", 0, func() { sinkS = Get[string](fast, "Name") }},
{"Get[int]", 0, func() { sinkI = Get[int](fast, "Mid.Inner.Zip") }},
{"Get[int64]", 0, func() { sinkI64 = Get[int64](fast, "Mid.Inner.Zip") }},
{"Get[bool]", 0, func() { sinkB = Get[bool](fast, "Mid.Inner.Active") }},
{"Get[float64]", 0, func() { sinkF = Get[float64](fast, "Mid.Inner.Ratio") }},
{"Get[string] slice下标", 0, func() { sinkS = Get[string](fast, "Tags", "1") }},
{"Get[string] 路径不存在", 0, func() { sinkS = Get[string](fast, "NoSuch") }},
{"Get[string] 未导出字段", 0, func() { sinkS = Get[string](fast, "secret") }},
{"Get[int] 路径不存在", 0, func() { sinkI = Get[int](fast, "NoSuch") }},
// ---- 会分配的分支(预期之内, 但要说清楚) ----
{"Get[string] 跨类型(int=42, 命中strconv小整数缓存)", -1, func() { sinkS = Get[string](fastBox, "I") }},
{"Get[string] 跨类型(int=100000, 真实格式化)", -1, func() { sinkS = Get[string](fast, "Mid.Inner.Zip") }},
{"Get[string] 跨类型(float64)", -1, func() { sinkS = Get[string](fast, "Mid.Inner.Ratio") }},
{"Get[int] 跨类型(string字段)", -1, func() { sinkI = Get[int](fastBox, "NumStr") }},
{"Get[string] map键", -1, func() { sinkS = Get[string](fast, "Meta", "k1") }},
{"Get[[]string] default分支", -1, func() { sinkSS = Get[[]string](fast, "Tags") }},
{"Get[string] 回退到reflux", -1, func() { sinkS = Get[string](slow, "Mid.Inner.City") }},
// ---- 对照 ----
{"对照: Get(...).String()", -1, func() { sinkS = fast.Get("Mid", "Inner", "City").String() }},
{"对照: 内部 getString", -1, func() { sinkS = fast.(*rfx).getString("Mid", "Inner", "City") }},
{"对照: 参照实现 refx Get().String()", -1, func() { sinkS = slow.Get("Mid", "Inner", "City").String() }},
}
t.Log("每次调用的分配次数:")
for _, c := range cases {
got := testing.AllocsPerRun(200, c.f)
t.Logf(" %-32s %.0f allocs", c.name, got)
if c.want >= 0 && got != c.want {
t.Errorf("%s: 分配 %.0f 次, 期望 %.0f 次", c.name, got, c.want)
}
}
}
+1009
View File
File diff suppressed because it is too large Load Diff
+215
View File
@@ -0,0 +1,215 @@
// 类型布局描述符: 把 struct 的字段偏移量、元素类型大小等信息构建一次并全局缓存,
// 热路径上不再走 reflect 的按名字段查找。
package reflux
import (
"reflect"
"sync"
)
// rfxFieldDesc 描述一个字段在其所属 struct 里的位置和类型。
type rfxFieldDesc struct {
// Name 字段名(原始声明名)
Name string
// Offset 字段相对于 struct 起始地址的字节偏移量
Offset uintptr
// Type 字段类型的描述符, 递归引用以支持嵌套 struct
Type *rfxTypeDesc
// Exported 字段是否是导出字段。
//
// 这是本原型最关键的安全标记: reflect.NewAt 构造出来的 Value 是可寻址且
// **可写**的, 它不带 reflect 正常路径上的 flagRO 只读标记, 也就是说 Go 语言
// 层面"不能修改未导出字段"这条保护在 unsafe 路径上完全失效。因此必须由我们
// 自己在每一次 Get/Set 前显式拒绝未导出字段。
Exported bool
// index 在 reflect.Type 里的字段下标, 回退到 reflect 路径时使用
index int
}
// rfxTypeDesc 描述一个类型的内存布局, 构建一次之后全局复用。
type rfxTypeDesc struct {
rtype reflect.Type
Kind reflect.Kind
Size uintptr
// fields 按声明顺序保存所有字段, 用于线性扫描查找
fields []rfxFieldDesc
// fieldMap 按字段名索引, 用于 map 查找。
// 除了原始字段名, 还额外收录了"首字母小写"的别名, 以兼容 reflux 的
// tryStructFieldValue 行为(Get("address") 能命中 Address 字段)。
fieldMap map[string]*rfxFieldDesc
// Elem 是 ptr/slice/array 的元素类型描述符
Elem *rfxTypeDesc
// elemSize 是元素类型的字节大小, slice/array 下标寻址用
elemSize uintptr
// arrayLen 是数组长度(仅 Kind == Array 时有效)
arrayLen int
// ptrElemKind 缓存指针指向类型的 Kind, 避免热路径上再取一次
ptrElemKind reflect.Kind
// fast 标记该类型是否是可以直接按机器类型读写的常见标量类型。
// 热路径上只要比较一个字节, 就能决定走不走零分配快路径。
fast fastKind
}
// fastKind 标记"类型与 Go 内置标量完全一致"的情况。
// 注意必须是**完全一致**: type MyStr string 的 Kind 也是 String, 但直接按
// string 写入会绕过 reflux 原有的 cast 转换语义, 所以这里只认原生类型。
type fastKind uint8
const (
fkNone fastKind = iota
fkString
fkInt
fkBool
fkFloat64
)
var (
typeString = reflect.TypeOf("")
typeInt = reflect.TypeOf(int(0))
typeBool = reflect.TypeOf(false)
typeFloat64 = reflect.TypeOf(float64(0))
)
func fastKindOf(t reflect.Type) fastKind {
switch t {
case typeString:
return fkString
case typeInt:
return fkInt
case typeBool:
return fkBool
case typeFloat64:
return fkFloat64
}
return fkNone
}
// isScalar 报告该类型是不是可以用 cast 直接转换的标量。
// 只有标量目标才走 fastx 自己的赋值逻辑; 复合类型(指针/切片/结构体/map/interface)
// 的赋值语义由 reflux.setValue 定义, 重新实现必然产生偏差, 一律交回它处理。
func (td *rfxTypeDesc) isScalar() bool {
switch td.Kind {
case reflect.String, reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Uintptr, reflect.Float32, reflect.Float64:
return true
}
return false
}
// lookupField 用 map 查找字段, 未找到返回 nil。
func (td *rfxTypeDesc) lookupField(name string) *rfxFieldDesc {
return td.fieldMap[name]
}
var (
// descCache 是全局类型描述符缓存。reflect.Type 本身可比较, 直接做 key。
descCache sync.Map // map[reflect.Type]*rfxTypeDesc
// descMu 只在缓存未命中(冷路径)时加锁, 保证同一时刻只有一个构建者,
// 并且保证带环的类型图整体构建完成后才发布, 读者永远看不到半成品。
descMu sync.Mutex
)
// rfxDescriptorOf 返回类型 t 的布局描述符, 第一次遇到时构建, 之后命中缓存。
func rfxDescriptorOf(t reflect.Type) *rfxTypeDesc {
if v, ok := descCache.Load(t); ok {
return v.(*rfxTypeDesc)
}
descMu.Lock()
defer descMu.Unlock()
// 双重检查: 可能在抢锁期间已经被别的 goroutine 构建好了
if v, ok := descCache.Load(t); ok {
return v.(*rfxTypeDesc)
}
// pending 保存本次构建过程中新建的所有描述符。自引用类型
// (type Node struct{ Next *Node }) 靠它终止递归。
pending := make(map[reflect.Type]*rfxTypeDesc, 8)
td := buildLocked(t, pending)
// 整张类型图都填充完毕之后再统一发布。sync.Map.Store 提供的
// happens-before 保证了读者 Load 到任何一个描述符时, 它引用到的
// 其它描述符也已经写完。
for k, v := range pending {
descCache.Store(k, v)
}
return td
}
// buildLocked 在持有 descMu 的前提下递归构建描述符。
func buildLocked(t reflect.Type, pending map[reflect.Type]*rfxTypeDesc) *rfxTypeDesc {
if v, ok := descCache.Load(t); ok {
return v.(*rfxTypeDesc)
}
if td, ok := pending[t]; ok {
return td
}
td := &rfxTypeDesc{
rtype: t,
Kind: t.Kind(),
Size: t.Size(),
fast: fastKindOf(t),
}
// 先登记再递归, 否则自引用类型会栈溢出
pending[t] = td
switch t.Kind() {
case reflect.Struct:
n := t.NumField()
td.fields = make([]rfxFieldDesc, n)
td.fieldMap = make(map[string]*rfxFieldDesc, n*2)
for i := 0; i < n; i++ {
sf := t.Field(i)
td.fields[i] = rfxFieldDesc{
Name: sf.Name,
Offset: sf.Offset,
Type: buildLocked(sf.Type, pending),
Exported: sf.IsExported(),
index: i,
}
}
for i := range td.fields {
f := &td.fields[i]
td.fieldMap[f.Name] = f
}
// 追加"首字母小写"别名, 兼容 reflux 的大小写宽松查找。
// 只在不与真实字段名冲突时才登记。
for i := range td.fields {
f := &td.fields[i]
if len(f.Name) > 0 && f.Name[0] >= 'A' && f.Name[0] <= 'Z' {
alias := string(f.Name[0]+32) + f.Name[1:]
if _, exists := td.fieldMap[alias]; !exists {
td.fieldMap[alias] = f
}
}
}
case reflect.Ptr:
td.Elem = buildLocked(t.Elem(), pending)
td.ptrElemKind = t.Elem().Kind()
case reflect.Slice:
td.Elem = buildLocked(t.Elem(), pending)
td.elemSize = t.Elem().Size()
case reflect.Array:
td.Elem = buildLocked(t.Elem(), pending)
td.elemSize = t.Elem().Size()
td.arrayLen = t.Len()
case reflect.Map:
// map 没有稳定的内存布局可以做偏移量运算, 描述符只记录元素类型,
// 实际取值走 reflect 回退路径。
td.Elem = buildLocked(t.Elem(), pending)
}
return td
}
+516
View File
@@ -0,0 +1,516 @@
package reflux
// 新实现(rfx)特有的行为验证: 未导出字段不可达、map 元素副本语义、
// 地址别名、自引用类型的描述符构建、并发安全。
import (
"encoding/json"
"reflect"
"strings"
"sync"
"testing"
)
type uxInner struct {
City string
Zip int
Active bool
Ratio float64
}
type uxMiddle struct {
Inner uxInner
InnerPtr *uxInner
Any any
}
type uxOuter struct {
Name string
Mid uxMiddle
Tags []string
Nums [3]int
Meta map[string]string
Objs map[string]uxInner
secret string // 未导出字段, 必须无法读写
Structs []uxInner
}
func newUxOuter() *uxOuter {
return &uxOuter{
Name: "root",
Mid: uxMiddle{
Inner: uxInner{City: "Beijing", Zip: 100000, Active: true, Ratio: 1.5},
InnerPtr: &uxInner{City: "Shanghai", Zip: 200000},
Any: uxInner{City: "Shenzhen", Zip: 518000},
},
Tags: []string{"a", "b", "c"},
Nums: [3]int{10, 20, 30},
Meta: map[string]string{"k1": "v1", "k2": "v2"},
Objs: map[string]uxInner{"o1": {City: "Chengdu", Zip: 610000}},
secret: "should-not-be-reachable",
Structs: []uxInner{{City: "Wuhan"}, {City: "Xian"}},
}
}
// ---------------------------------------------------------------------------
// Get: 各种路径形态
// ---------------------------------------------------------------------------
func TestGetPaths(t *testing.T) {
o := newUxOuter()
r := New(o)
cases := []struct {
name string
path []string
want string
}{
{"单层字段", []string{"Name"}, "root"},
{"多层嵌套", []string{"Mid", "Inner", "City"}, "Beijing"},
{"点号路径", []string{"Mid.Inner.City"}, "Beijing"},
{"点号混合", []string{"Mid.Inner", "City"}, "Beijing"},
{"指针自动解引用", []string{"Mid", "InnerPtr", "City"}, "Shanghai"},
{"interface 自动解引用", []string{"Mid", "Any", "City"}, "Shenzhen"},
{"slice 下标", []string{"Tags", "1"}, "b"},
{"array 下标", []string{"Nums", "2"}, "30"},
{"map 键", []string{"Meta", "k1"}, "v1"},
{"map 里的 struct", []string{"Objs", "o1", "City"}, "Chengdu"},
{"slice 里的 struct", []string{"Structs", "1", "City"}, "Xian"},
{"首字母小写兼容", []string{"mid", "inner", "city"}, "Beijing"},
{"前后多余点号", []string{".Mid..Inner.City."}, "Beijing"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := r.Get(c.path...).String()
if got != c.want {
t.Fatalf("Get(%q) = %q, 期望 %q", c.path, got, c.want)
}
// 与 reflux 现有实现逐条对齐
if want := newLegacy(o).Get(c.path...).String(); want != got {
t.Fatalf("Get(%q): fastx=%q reflux=%q, 两者行为不一致", c.path, got, want)
}
})
}
}
func TestGetMissing(t *testing.T) {
o := newUxOuter()
r := New(o)
missing := [][]string{
{"NoSuchField"},
{"Mid", "Inner", "NoSuchField"},
{"Tags", "99"},
{"Tags", "notanumber"},
{"Nums", "5"},
{"Meta", "nokey"},
{"Name", "City"}, // 在标量上继续取路径
}
for _, p := range missing {
got := r.Get(p...)
if got.Exists() {
t.Fatalf("Get(%q) 不应该存在, 却返回了 %v", p, got.Any())
}
if got.String() != "" {
t.Fatalf("Get(%q).String() 应该是空串, 得到 %q", p, got.String())
}
}
}
// 未导出字段既不能读也不能写 —— 这是 unsafe 路径上最关键的安全断言。
func TestUnexportedFieldIsUnreachable(t *testing.T) {
o := newUxOuter()
r := New(o)
if r.Get("secret").Exists() {
t.Fatal("未导出字段 secret 不应该可读")
}
err := uxMustPanic(t, func() { r.Set("secret", "hacked") })
if err == nil {
t.Fatal("向未导出字段写入应该 panic")
}
if o.secret != "should-not-be-reachable" {
t.Fatalf("未导出字段被改写了: %q", o.secret)
}
}
// ---------------------------------------------------------------------------
// Set
// ---------------------------------------------------------------------------
func TestUnsafeSet(t *testing.T) {
o := newUxOuter()
r := New(o)
r.Set("Name", "changed")
if o.Name != "changed" {
t.Fatalf("Set(Name) 失败: %q", o.Name)
}
r.Set("Mid.Inner.City", "Hangzhou")
if o.Mid.Inner.City != "Hangzhou" {
t.Fatalf("Set 嵌套字段失败: %q", o.Mid.Inner.City)
}
r.Set("Mid.Inner.Zip", 330000)
if o.Mid.Inner.Zip != 330000 {
t.Fatalf("Set int 失败: %d", o.Mid.Inner.Zip)
}
r.Set("Mid.Inner.Active", false)
if o.Mid.Inner.Active {
t.Fatal("Set bool 失败")
}
r.Set("Mid.Inner.Ratio", 2.5)
if o.Mid.Inner.Ratio != 2.5 {
t.Fatalf("Set float64 失败: %v", o.Mid.Inner.Ratio)
}
// 通过指针字段设置
r.Set("Mid.InnerPtr.City", "Nanjing")
if o.Mid.InnerPtr.City != "Nanjing" {
t.Fatalf("经指针 Set 失败: %q", o.Mid.InnerPtr.City)
}
// slice 元素
r.Set("Tags.0", "z")
if o.Tags[0] != "z" {
t.Fatalf("Set slice 元素失败: %v", o.Tags)
}
// array 元素
r.Set("Nums.1", 99)
if o.Nums[1] != 99 {
t.Fatalf("Set array 元素失败: %v", o.Nums)
}
// map 键(新增 + 覆盖)
r.Set("Meta.k1", "v1-new")
r.Set("Meta.k3", "v3")
if o.Meta["k1"] != "v1-new" || o.Meta["k3"] != "v3" {
t.Fatalf("Set map 键失败: %v", o.Meta)
}
// slice 里的 struct 字段
r.Set("Structs.0.City", "Changsha")
if o.Structs[0].City != "Changsha" {
t.Fatalf("Set slice 内 struct 字段失败: %v", o.Structs)
}
}
// 类型不完全一致时应该走 cast 回退, 与 reflux 行为一致
func TestUnsafeSetTypeConversion(t *testing.T) {
o := newUxOuter()
r := New(o)
r.Set("Mid.Inner.Zip", "123456") // string -> int
if o.Mid.Inner.Zip != 123456 {
t.Fatalf("string->int 转换失败: %d", o.Mid.Inner.Zip)
}
r.Set("Name", 42) // int -> string
if o.Name != "42" {
t.Fatalf("int->string 转换失败: %q", o.Name)
}
r.Set("Mid.Inner.Ratio", 3) // int -> float64
if o.Mid.Inner.Ratio != 3 {
t.Fatalf("int->float64 转换失败: %v", o.Mid.Inner.Ratio)
}
}
func TestUnsafeSetErrors(t *testing.T) {
o := newUxOuter()
r := New(o)
// 路径不存在
if err := uxMustPanic(t, func() { r.Set("NoSuchField", 1) }); err == nil {
t.Fatal("向不存在的字段写入应该 panic")
}
// 父路径不存在
if err := uxMustPanic(t, func() { r.Set("NoSuch.Deep.Path", 1) }); err == nil {
t.Fatal("父路径不存在时应该 panic")
}
// 空路径
if err := uxMustPanic(t, func() { r.Set("", 1) }); err == nil {
t.Fatal("空路径应该 panic")
}
// slice 越界
if err := uxMustPanic(t, func() { r.Set("Tags.99", "x") }); err == nil {
t.Fatal("slice 越界应该 panic")
}
// 无法转换的类型
if err := uxMustPanic(t, func() { r.Set("Mid.Inner.Zip", struct{}{}) }); err == nil {
t.Fatal("无法转换的类型应该 panic")
}
}
// 从 map 里取出来的 struct 是副本, 对它的写入不应该影响原 map ——
// 这与 reflect 里 "map 元素不可寻址" 的语义一致。
func TestMapValueIsCopy(t *testing.T) {
o := newUxOuter()
r := New(o)
got := r.Get("Objs", "o1")
if !got.Exists() {
t.Fatal("Objs.o1 应该存在")
}
err := uxMustPanic(t, func() { got.Set("City", "Modified") })
if err == nil {
t.Log("对 map 元素副本的写入未 panic(落在副本上)")
}
if o.Objs["o1"].City != "Chengdu" {
t.Fatalf("原 map 里的值被改写了: %q", o.Objs["o1"].City)
}
}
// 传值(非指针)时不应该影响调用方的原始数据
func TestNewByValueDoesNotMutateOriginal(t *testing.T) {
o := *newUxOuter()
r := New(o)
r.Set("Name", "changed")
if o.Name != "root" {
t.Fatalf("传值构造时原始数据被改写了: %q", o.Name)
}
if r.Get("Name").String() != "changed" {
t.Fatal("副本上的写入没生效")
}
}
// ---------------------------------------------------------------------------
// Accessor / JSON / 其它 R 方法
// ---------------------------------------------------------------------------
func TestAccessors(t *testing.T) {
o := newUxOuter()
r := New(o)
if got := r.Get("Mid.Inner.City").String(); got != "Beijing" {
t.Fatalf("String() = %q", got)
}
if got := r.Get("Mid.Inner.Zip").Int(); got != 100000 {
t.Fatalf("Int() = %d", got)
}
if got := r.Get("Mid.Inner.Zip").Int64(); got != 100000 {
t.Fatalf("Int64() = %d", got)
}
if got := r.Get("Mid.Inner.Active").Bool(); !got {
t.Fatal("Bool() = false")
}
if got := r.Get("Mid.Inner.Ratio").Float64(); got != 1.5 {
t.Fatalf("Float64() = %v", got)
}
if got := r.Get("Mid.Inner.Ratio").Float32(); got != 1.5 {
t.Fatalf("Float32() = %v", got)
}
if got := r.Get("Mid.Inner.Zip").String(); got != "100000" {
t.Fatalf("int 转 String() = %q", got)
}
// Raw / Ptr
raw := r.Get("Mid.Inner.City").Raw()
if !raw.IsValid() || raw.Kind() != reflect.String || raw.String() != "Beijing" {
t.Fatalf("Raw() = %v", raw)
}
ptr, ok := r.Get("Mid.Inner.City").Ptr().(*string)
if !ok || *ptr != "Beijing" {
t.Fatalf("Ptr() = %#v", r.Get("Mid.Inner.City").Ptr())
}
// Ptr 拿到的必须是原始数据的地址, 改它能反映到原对象上
*ptr = "ViaPtr"
if o.Mid.Inner.City != "ViaPtr" {
t.Fatal("Ptr() 返回的不是原始数据的地址")
}
// Any
if got := r.Get("Mid.Inner.Zip").Any(); got != 100000 {
t.Fatalf("Any() = %#v", got)
}
// StringSlice / Slice
if got := r.Get("Tags").StringSlice(); !reflect.DeepEqual(got, []string{"a", "b", "c"}) {
t.Fatalf("StringSlice() = %v", got)
}
if got := r.Get("Meta").StringMapString(); got["k1"] != "v1" {
t.Fatalf("StringMapString() = %v", got)
}
}
func TestExistsKeysArray(t *testing.T) {
o := newUxOuter()
r := New(o)
if !r.Exists("Mid", "Inner", "City") {
t.Fatal("Exists 应该为 true")
}
if r.Exists("Mid", "Nope") {
t.Fatal("Exists 应该为 false")
}
keys := r.Keys()
if len(keys) == 0 || keys[0] != "Name" {
t.Fatalf("Keys() = %v", keys)
}
arr := r.Get("Tags").Array()
if len(arr) != 3 || arr[2].String() != "c" {
t.Fatalf("Array() = %v", arr)
}
// Array 返回的元素应该指向原始底层数组, 对它的写入能反映到原对象上
objs := r.Get("Structs").Array()
if len(objs) != 2 {
t.Fatalf("Structs.Array() 长度 = %d", len(objs))
}
objs[0].Set("City", "Guiyang")
if o.Structs[0].City != "Guiyang" {
t.Fatalf("Array() 元素没有指向原始底层数组: %+v", o.Structs)
}
}
func TestLookup(t *testing.T) {
o := newUxOuter()
r := New(o)
acc, ok := r.Lookup("Mid.Inner.City")
if !ok || acc.String() != "Beijing" {
t.Fatalf("Lookup = %v %v", acc, ok)
}
if _, ok := r.Lookup("Nope"); ok {
t.Fatal("Lookup 不存在的路径应该返回 false")
}
if got := r.MustLookup("Nope").String(); got != "" {
t.Fatalf("MustLookup 不存在时应该返回零值, 得到 %q", got)
}
}
func TestJSON(t *testing.T) {
o := newUxOuter()
r := New(o)
b, err := json.Marshal(r.Get("Mid", "Inner"))
if err != nil {
t.Fatalf("Marshal 失败: %v", err)
}
if !strings.Contains(string(b), `"City":"Beijing"`) {
t.Fatalf("Marshal 结果不对: %s", b)
}
target := r.Get("Mid", "Inner")
if err := json.Unmarshal([]byte(`{"City":"Kunming","Zip":650000}`), target); err != nil {
t.Fatalf("Unmarshal 失败: %v", err)
}
if o.Mid.Inner.City != "Kunming" || o.Mid.Inner.Zip != 650000 {
t.Fatalf("Unmarshal 没有写回原对象: %+v", o.Mid.Inner)
}
}
// ---------------------------------------------------------------------------
// TypeDescriptor 本身
// ---------------------------------------------------------------------------
type uxSelfRef struct {
Name string
Next *uxSelfRef
Kids []uxSelfRef
}
// 自引用类型不能让描述符构建无限递归
func TestSelfReferentialType(t *testing.T) {
done := make(chan struct{})
go func() {
defer close(done)
td := rfxDescriptorOf(reflect.TypeOf(uxSelfRef{}))
if len(td.fields) != 3 {
t.Errorf("字段数 = %d", len(td.fields))
}
}()
<-done
root := &uxSelfRef{Name: "a", Next: &uxSelfRef{Name: "b", Next: &uxSelfRef{Name: "c"}}}
r := New(root)
if got := r.Get("Next.Next.Name").String(); got != "c" {
t.Fatalf("自引用类型路径遍历失败: %q", got)
}
}
// ---------------------------------------------------------------------------
// 并发
// ---------------------------------------------------------------------------
type uxRaceA struct {
X string
B uxRaceB
}
type uxRaceB struct {
Y string
C uxRaceC
}
type uxRaceC struct{ Z string }
// 多个 goroutine 同时构建描述符 + 读写各自独立的实例
func TestConcurrentAccess(t *testing.T) {
const n = 32
var wg sync.WaitGroup
wg.Add(n)
for i := 0; i < n; i++ {
go func(i int) {
defer wg.Done()
// 每个 goroutine 操作自己的实例, 但共享同一份全局描述符缓存
a := &uxRaceA{X: "x", B: uxRaceB{Y: "y", C: uxRaceC{Z: "z"}}}
r := New(a)
for j := 0; j < 200; j++ {
if got := r.Get("B.C.Z").String(); got != "z" && got != "changed" {
t.Errorf("并发读到了意外的值: %q", got)
return
}
r.Set("B.C.Z", "changed")
r.Set("B.C.Z", "z")
}
// 也并发构建一些新类型的描述符
switch i % 4 {
case 0:
rfxDescriptorOf(reflect.TypeOf(uxOuter{}))
case 1:
rfxDescriptorOf(reflect.TypeOf(uxSelfRef{}))
case 2:
rfxDescriptorOf(reflect.TypeOf(uxMiddle{}))
case 3:
rfxDescriptorOf(reflect.TypeOf(uxOuter{}))
}
}(i)
}
wg.Wait()
}
// 同一个实例被多个 goroutine 并发只读, 不应该有竞争
func TestConcurrentReadSameInstance(t *testing.T) {
a := &uxRaceA{X: "x", B: uxRaceB{Y: "y", C: uxRaceC{Z: "z"}}}
r := New(a)
var wg sync.WaitGroup
for i := 0; i < 16; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 500; j++ {
if got := r.Get("B", "C", "Z").String(); got != "z" {
t.Errorf("并发只读得到 %q", got)
return
}
}
}()
}
wg.Wait()
}
// ---------------------------------------------------------------------------
func uxMustPanic(t *testing.T, f func()) (recovered any) {
t.Helper()
defer func() { recovered = recover() }()
f()
return nil
}
+125
View File
@@ -0,0 +1,125 @@
package reflux
import (
"reflect"
"unsafe"
)
// 本文件是整个 reflux 包里唯一使用 unsafe 的地方, 评审内存安全性时只需要看这里
// 加上 value.go 里对这些函数的调用点。
//
// 本包 unsafe 路径的核心不变式(invariant), 下面每个函数都依赖它:
//
// I1. value.ptr 永远是一个"指向某个 value.td.rtype 类型值的合法地址",
// 要么来自 reflect.Value.UnsafePointer()/reflect.New(), 要么由
// fieldAt/elemAt 从一个满足 I1 的地址加上该类型真实的字段偏移量算出。
// I2. ptr 是 unsafe.Pointer 类型而不是 uintptr, GC 能正常追踪它,
// 被指向的对象不会在 value 存活期间被回收或移动。
// 所有指针运算都用 unsafe.Add 在单个表达式内完成, 不把地址存成 uintptr。
// I3. 只有 Exported == true 的字段才允许构造出 value, 见 rfxFieldDesc.Exported
// 的注释: reflect.NewAt 不带只读标记, 语言层面的未导出字段保护在这里失效,
// 必须由调用方自己拦住。
// I4. 只有 writable == true 的 value 才允许写入, writable 的传播规则见
// value.go 的 canWrite 说明。
// fieldAt 返回 base 所指 struct 里偏移量为 off 的字段地址。
// 要求: base 满足 I1, off 来自 reflect.StructField.Offset。
func fieldAt(base unsafe.Pointer, off uintptr) unsafe.Pointer {
return unsafe.Add(base, off)
}
// loadPtr 把 p 当作 *T 读出来, 返回它指向的地址。
// 要求: p 指向一个指针类型的字段。返回 nil 表示空指针。
func loadPtr(p unsafe.Pointer) unsafe.Pointer {
return *(*unsafe.Pointer)(p)
}
// sliceHeader 与运行时 slice 的内存布局一致, 用于零拷贝读取切片头。
type sliceHeader struct {
Data unsafe.Pointer
Len int
Cap int
}
// sliceLen 返回 p 所指切片的长度。要求 p 指向一个 slice 类型的值。
func sliceLen(p unsafe.Pointer) int {
return (*sliceHeader)(p).Len
}
// sliceElemAt 返回 p 所指切片第 i 个元素的地址, 越界返回 nil。
// 要求: elemSize 是该切片元素类型的真实 Size()。
func sliceElemAt(p unsafe.Pointer, i int, elemSize uintptr) unsafe.Pointer {
h := (*sliceHeader)(p)
if i < 0 || i >= h.Len {
return nil
}
return unsafe.Add(h.Data, uintptr(i)*elemSize)
}
// arrayElemAt 返回 p 所指数组第 i 个元素的地址, 越界返回 nil。
func arrayElemAt(p unsafe.Pointer, i int, n int, elemSize uintptr) unsafe.Pointer {
if i < 0 || i >= n {
return nil
}
return unsafe.Add(p, uintptr(i)*elemSize)
}
// valueAt 把 (地址, 类型) 还原成一个可寻址的 reflect.Value, 用于回退到
// 标准反射路径(类型转换、JSON、map 操作等)。
//
// 安全性: 返回的 Value 是可寻址且可写的, 即使它对应的是未导出字段 ——
// 这正是 I3 存在的原因, 调用方必须已经确认过 Exported。
func valueAt(td *rfxTypeDesc, p unsafe.Pointer) reflect.Value {
return reflect.NewAt(td.rtype, p).Elem()
}
// ptrValueAt 把 (地址, 类型) 还原成一个 *T 的 reflect.Value。
func ptrValueAt(td *rfxTypeDesc, p unsafe.Pointer) reflect.Value {
return reflect.NewAt(td.rtype, p)
}
// addrOf 取一个可寻址 reflect.Value 的地址。要求 v.CanAddr()。
func addrOf(v reflect.Value) unsafe.Pointer {
return v.Addr().UnsafePointer()
}
// boxCopy 把一个不可寻址的值(典型来源: map 取值、interface 拆包)拷贝到
// 新分配的堆内存上, 返回该副本的地址。副本与原值不共享内存, 因此对它的写入
// 不会影响原容器 —— 这与 reflect 里 "map 元素不可寻址" 的语义一致。
func boxCopy(v reflect.Value) unsafe.Pointer {
p := reflect.New(v.Type())
p.Elem().Set(v)
return p.UnsafePointer()
}
// ---- 基础类型的直接读写 ----
// 这些函数通过 *T 做**有类型**的读写, 编译器会正常插入 GC 写屏障,
// 不存在漏掉写屏障导致对象被误回收的问题。unsafe 只体现在"地址是算出来的"。
func loadString(p unsafe.Pointer) string { return *(*string)(p) }
func storeString(p unsafe.Pointer, v string) { *(*string)(p) = v }
func loadInt(p unsafe.Pointer) int { return *(*int)(p) }
func storeInt(p unsafe.Pointer, v int) { *(*int)(p) = v }
// 各宽度整数的直读。读取只依赖字段的 Kind(内存宽度), 不要求具名类型完全一致 ——
// type Age int 和 int 的内存表示相同, 按 *int 读出来的值是一样的。
// (写入则不同, 见 storeFast: 写要求类型完全一致, 否则可能绕过 cast 的转换语义。)
func loadInt8(p unsafe.Pointer) int8 { return *(*int8)(p) }
func loadInt16(p unsafe.Pointer) int16 { return *(*int16)(p) }
func loadInt32(p unsafe.Pointer) int32 { return *(*int32)(p) }
func loadInt64(p unsafe.Pointer) int64 { return *(*int64)(p) }
func loadUint(p unsafe.Pointer) uint { return *(*uint)(p) }
func loadUint8(p unsafe.Pointer) uint8 { return *(*uint8)(p) }
func loadUint16(p unsafe.Pointer) uint16 { return *(*uint16)(p) }
func loadUint32(p unsafe.Pointer) uint32 { return *(*uint32)(p) }
func loadUint64(p unsafe.Pointer) uint64 { return *(*uint64)(p) }
func loadFloat32(p unsafe.Pointer) float32 { return *(*float32)(p) }
func loadBool(p unsafe.Pointer) bool { return *(*bool)(p) }
func storeBool(p unsafe.Pointer, v bool) { *(*bool)(p) = v }
func loadFloat64(p unsafe.Pointer) float64 { return *(*float64)(p) }
func storeFloat64(p unsafe.Pointer, v float64) { *(*float64)(p) = v }
+15 -4
View File
@@ -350,13 +350,23 @@ func tryMapFieldKey(m reflect.Value, key string) reflect.Value {
keyType := m.Type().Key() keyType := m.Type().Key()
// 这里必须把 key 复制到一个**新的局部变量**里,而且不能复用参数名。
//
// reflect.ValueOf 会把字符串装箱进 interface,逃逸分析因此判定传进来的变量
// 逃逸到堆。而 key 是从调用方的路径参数里切出来的子串 —— 一旦参数本身被判
// 定为 leaking param,Get("Address","City") 里那个可变参数切片就必须堆分配,
// 即使这次调用根本没走到 map 分支。
// 复制一次切断数据流,代价只落在真正访问 map 的路径上(那条路径本来就要为
// 取出来的 map 元素分配内存)。
k := strings.Clone(key)
// 构造原始 key // 构造原始 key
var mapKey reflect.Value var mapKey reflect.Value
if keyType.Kind() == reflect.String { if keyType.Kind() == reflect.String {
mapKey = reflect.ValueOf(key) mapKey = reflect.ValueOf(k)
} else { } else {
// 非 string 键,尽量使用原始字符串构造可转换的 key // 非 string 键,尽量使用原始字符串构造可转换的 key
raw := reflect.ValueOf(key) raw := reflect.ValueOf(k)
if raw.Type().AssignableTo(keyType) { if raw.Type().AssignableTo(keyType) {
mapKey = raw mapKey = raw
} else if raw.Type().ConvertibleTo(keyType) { } else if raw.Type().ConvertibleTo(keyType) {
@@ -373,8 +383,9 @@ func tryMapFieldKey(m reflect.Value, key string) reflect.Value {
} }
// 如果键类型是 string 且首字母大写,尝试首字母小写版本 // 如果键类型是 string 且首字母大写,尝试首字母小写版本
if keyType.Kind() == reflect.String && len(key) > 0 && unicode.IsUpper(rune(key[0])) { // (这里同样用副本 k,避免 key 被判定逃逸)
lowercased := lowercaseFirst(key) if keyType.Kind() == reflect.String && len(k) > 0 && unicode.IsUpper(rune(k[0])) {
lowercased := lowercaseFirst(k)
mapKey = reflect.ValueOf(lowercased) mapKey = reflect.ValueOf(lowercased)
val = m.MapIndex(mapKey) val = m.MapIndex(mapKey)
if val.IsValid() { if val.IsValid() {