Files
reflux/rfx_contract_test.go
what 5c9f8bd6e7 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 处行为偏差。
2026-08-28 16:37:04 +08:00

254 lines
9.9 KiB
Go

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)
}
})
}
}