将实验目录 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 处行为偏差。
254 lines
9.9 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|