Files
reflux/rfx_unsafe_test.go
T
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

517 lines
14 KiB
Go

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
}