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:
@@ -19,7 +19,8 @@ Reflux 是一个 Go 语言包,提供了统一的接口用于访问和操作嵌
|
||||
- 🎯 **类型安全**: 使用反射但保证类型安全
|
||||
- 🔥 **增强类型转换**: 支持切片和结构体之间的智能转换(如 []any -> []T, map -> struct)
|
||||
- 🌀 **R 接口集成**: 支持直接传入 R 接口或 []R 切片,无缝集成反射值
|
||||
- 🚀 **高性能**: 优化的反射操作,低内存开销
|
||||
- ⚡ **泛型直取**: `reflux.Get[string](r, "Address", "City")` 直接返回目标类型,**零内存分配**
|
||||
- 🚀 **高性能**: 类型布局缓存 + 指针偏移寻址,Get/Set 比逐次反射快 3~4 倍,访问器快 8~12 倍
|
||||
- 📦 **零依赖**: 仅依赖 Go 标准库和 spf13/cast
|
||||
|
||||
## 安装
|
||||
@@ -1063,6 +1064,50 @@ value := rfx.Get("Config.Database.Host").String()
|
||||
|
||||
## 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 接口
|
||||
|
||||
```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 值传递**:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -64,7 +64,12 @@ func New(v any) R {
|
||||
case R:
|
||||
return t
|
||||
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:
|
||||
if t == valuex.Nil {
|
||||
return Nil
|
||||
@@ -112,9 +117,33 @@ func New(v any) R {
|
||||
// 如果原始传入的不是指针类型,需要进行深度克隆以避免修改原始数据
|
||||
// 对于引用类型(map, slice)这尤其重要
|
||||
if !isPtr {
|
||||
// 使用深度克隆创建一个完全独立的副本
|
||||
// 使用深度克隆创建一个完全独立的副本(DeepClone 返回的是指针)
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+215
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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 }
|
||||
@@ -350,13 +350,23 @@ func tryMapFieldKey(m reflect.Value, key string) reflect.Value {
|
||||
|
||||
keyType := m.Type().Key()
|
||||
|
||||
// 这里必须把 key 复制到一个**新的局部变量**里,而且不能复用参数名。
|
||||
//
|
||||
// reflect.ValueOf 会把字符串装箱进 interface,逃逸分析因此判定传进来的变量
|
||||
// 逃逸到堆。而 key 是从调用方的路径参数里切出来的子串 —— 一旦参数本身被判
|
||||
// 定为 leaking param,Get("Address","City") 里那个可变参数切片就必须堆分配,
|
||||
// 即使这次调用根本没走到 map 分支。
|
||||
// 复制一次切断数据流,代价只落在真正访问 map 的路径上(那条路径本来就要为
|
||||
// 取出来的 map 元素分配内存)。
|
||||
k := strings.Clone(key)
|
||||
|
||||
// 构造原始 key
|
||||
var mapKey reflect.Value
|
||||
if keyType.Kind() == reflect.String {
|
||||
mapKey = reflect.ValueOf(key)
|
||||
mapKey = reflect.ValueOf(k)
|
||||
} else {
|
||||
// 非 string 键,尽量使用原始字符串构造可转换的 key
|
||||
raw := reflect.ValueOf(key)
|
||||
raw := reflect.ValueOf(k)
|
||||
if raw.Type().AssignableTo(keyType) {
|
||||
mapKey = raw
|
||||
} else if raw.Type().ConvertibleTo(keyType) {
|
||||
@@ -373,8 +383,9 @@ func tryMapFieldKey(m reflect.Value, key string) reflect.Value {
|
||||
}
|
||||
|
||||
// 如果键类型是 string 且首字母大写,尝试首字母小写版本
|
||||
if keyType.Kind() == reflect.String && len(key) > 0 && unicode.IsUpper(rune(key[0])) {
|
||||
lowercased := lowercaseFirst(key)
|
||||
// (这里同样用副本 k,避免 key 被判定逃逸)
|
||||
if keyType.Kind() == reflect.String && len(k) > 0 && unicode.IsUpper(rune(k[0])) {
|
||||
lowercased := lowercaseFirst(k)
|
||||
mapKey = reflect.ValueOf(lowercased)
|
||||
val = m.MapIndex(mapKey)
|
||||
if val.IsValid() {
|
||||
|
||||
Reference in New Issue
Block a user