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