perf: map 分段走 reflect, 离开后切回快路径
walk 逐跳调用 step, 而 step 的 map 分支每次都要 boxCopy —— map 元素不可寻址,
rfx 的表示又需要一个地址, 只能 reflect.New 复制一份。层级越深累积越多,
导致纯 map 路径比改造前的旧实现还慢, 是整个重写里唯一的性能回归。
改法要两头兼顾:
- 一路 reflect 走到底能省掉逐跳装箱, 但 map 后面的 struct 跳会丢掉偏移量
快路径、退回 FieldByName —— 实测 map→struct→struct 反而比逐跳装箱慢 5.8%
- 所以只在**连续的 map 段**内用 reflect, 一离开就装箱一次切回快路径
map→map→struct→struct 303.2ns/16a -> 205.6ns/10a 1.47x
map→struct→struct→map 309.0ns/16a -> 246.3ns/11a 1.25x
map→struct→struct 224.6ns/11a -> 140.3ns/ 6a 1.60x
纯 map 三层 300.9ns/20a -> 260.8ns/15a 1.15x (原 0.72x)
纯 map 单跳 120.0ns/ 8a -> 126.8ns/ 7a 0.95x
struct 三层(对照) 200.4ns/ 9a -> 59.6ns/ 2a 3.36x
单跳 map 仍慢 5%: 只有一跳时没有 boxCopy 可省, 而 valueAt 那次 reflect.NewAt
省不掉。其余形态全部转正。
两个实现上的坑, 都是测试抓出来的:
1. walkMapRun 发现已离开 map 段时, 那一跳已经从迭代器取走但还没用, 必须交还
给调用方。最初用 *it = save 回退, 结果**零分配快路径全线退化成 1 次分配** ——
通过指针写回会让逃逸分析认为 it 指向的内容可能逃逸, 进而判定 Get 的可变参数
切片逃逸, 波及所有路径, 连纯 struct 的路径都跟着中枪。改成把待处理的段
作为返回值交出去就好了。
2. 若干处清理: normalize 从 step 提到 walk(step 只有 walk 一个调用方);
移除 step 里已不可达的 Map 分支。
测试 252 个对拍用例覆盖 map 之后的各种后继类型与失败形态, 另加 19 个交界用例
专压"离开 map 段"那一跳。已验证有效: 把交还 pending 的那行改成丢弃后,
53 个用例失败。
This commit is contained in:
@@ -1236,23 +1236,19 @@ type Reflux interface {
|
||||
| 场景 | 旧版本 | 新版本 | 变化 |
|
||||
|---|---|---|---:|
|
||||
| `Get("Meta","k")` struct 字段 → map 键 | 175.6 ns / 10 allocs | 140.9 ns / 7 allocs | 1.25x |
|
||||
| **纯 map 路径** `Get("leaf")`(根就是 map) | 120.7 ns / 8 allocs | 161.7 ns / 8 allocs | **0.75x** |
|
||||
| **纯 map 路径** `Get("b","c","leaf")` 三层 | 314.4 ns / 20 allocs | 439.5 ns / 20 allocs | **0.72x** |
|
||||
| **纯 map 路径** `Get("leaf")`(根就是 map) | 120.0 ns / 8 allocs | 126.8 ns / 7 allocs | **0.95x** |
|
||||
| 纯 map 三层 | 300.9 ns / 20 allocs | 260.8 ns / 15 allocs | 1.15x |
|
||||
| map → struct → struct | 224.6 ns / 11 allocs | 140.3 ns / 6 allocs | 1.60x |
|
||||
| `New(指针)` 构造 | 16.9 ns / 1 alloc | 25.4 ns / 1 alloc | **0.67x** |
|
||||
|
||||
- **map 路径不但没提速,纯 map 场景反而更慢**。map 没有稳定的内存布局可以做
|
||||
偏移量运算,这条路径完全走 reflect;而且新实现的内部表示是"地址 + 类型描述符",
|
||||
而 map 元素**不可寻址**,所以每经过一跳 map 都要 `reflect.New` 拷贝一份到堆上
|
||||
才能拿到地址 —— 旧实现直接持有 `reflect.Value`,不需要这次拷贝。
|
||||
层级越深,多出来的拷贝越多。
|
||||
|
||||
上面第一行的 1.25x 之所以是正的,是因为第一跳 `Meta` 是 **struct 字段**,
|
||||
走了快路径,把第二跳 map 的损失盖过去了。**根是 map、或路径深处全是 map 时,
|
||||
收益是负的。**
|
||||
- **map 路径基本打平,提速有限**。map 没有稳定的内存布局可以做偏移量运算,
|
||||
这条路径完全走 reflect。命中 map 之后会一次性用 reflect 走完剩余路径、
|
||||
只在最后装箱一次(逐跳装箱的话每跳都要 `reflect.New` 拷贝一份,因为 map
|
||||
元素不可寻址),所以层级越深收益越明显;但单跳 map 省不掉那次
|
||||
`reflect.NewAt`,仍比旧实现慢约 5%。
|
||||
|
||||
**选型建议**: 如果你的数据以 `map[string]any` 为主(比如把数据库查询结果直接
|
||||
存成 map),那么这次重写对你没有收益,反而略有退化 —— 收益全部集中在
|
||||
**struct 字段访问**上。
|
||||
存成 map),这次重写对你收益很小 —— 提速几乎全部集中在 **struct 字段访问**上。
|
||||
|
||||
- **`New` 慢了约 8 ns**: 构造时要查一次类型描述符缓存。这是一次性成本,
|
||||
换来之后每次 `Get`/`Set` 省下 50-100 ns —— 只要构造后至少访问一次就是净赚。
|
||||
|
||||
+13
-11
@@ -150,20 +150,22 @@
|
||||
| Struct | 三层嵌套字段 | 199.1 ns / 9 allocs | 63.4 ns / 2 allocs | **3.14×** |
|
||||
| Slice | `Get("Tags","1")` | 132.6 ns / 7 allocs | 47.5 ns / 2 allocs | **2.79×** |
|
||||
| `Get[T]` | 泛型直取,跳过装箱 | — | 29.8 ns / **0 allocs** | **5.08×** |
|
||||
| Map | struct 字段 → map 键 | 175.6 ns / 10 allocs | 140.9 ns / 7 allocs | 1.25× |
|
||||
| Map | **纯 map,三层嵌套** | 314.4 ns / 20 allocs | 439.5 ns / 20 allocs | **0.72×** |
|
||||
| Map | struct 字段 → map 键 | 270.3 ns / 16 allocs | 210.2 ns / 11 allocs | 1.29× |
|
||||
| Map | 纯 map,三层嵌套 | 300.9 ns / 20 allocs | 260.8 ns / 15 allocs | 1.15× |
|
||||
| Map | 纯 map,单跳 | 120.0 ns / 8 allocs | 126.8 ns / 7 allocs | **0.95×** |
|
||||
| Map | map → struct → struct | 224.6 ns / 11 allocs | 140.3 ns / 6 allocs | 1.60× |
|
||||
| Map | map → map → struct → struct | 303.2 ns / 16 allocs | 205.6 ns / 10 allocs | 1.47× |
|
||||
|
||||
三点说明:
|
||||
两点说明:
|
||||
|
||||
1. **纯 map 路径是负收益。** 根就是 map、或路径深处全是 map 时,当前实现比旧版慢约 28%。
|
||||
原因在第一张图里直接看得到:每跳一次 map 都要 `boxCopy`。
|
||||
**收益全部集中在 struct 字段访问上** —— 如果你的数据以 `map[string]any` 为主,
|
||||
这次重写对你没有收益。
|
||||
1. **map 路径分段处理,只有单跳 map 略负。** 命中 map 后用 reflect 连续走完
|
||||
**这一段** map,离开时装箱一次再切回偏移量快路径。两头都要顾:逐跳装箱的话
|
||||
每跳 map 都要 `boxCopy`(元素不可寻址),连续多层会累积;但一路 reflect 走到底
|
||||
又会让 map 后面的 struct 跳丢掉快路径、退回 `FieldByName`。分段之后三种混合
|
||||
形态都是最优。只有"单跳 map"仍慢约 5% —— 没有 `boxCopy` 可省,而
|
||||
`reflect.NewAt` 那一次省不掉。
|
||||
|
||||
2. **那个 1.25× 容易看错。** 它的第一跳 `Meta` 是 **struct 字段**,走了快路径,
|
||||
把第二跳 map 的损失盖了过去 —— 不能当成"map 变快了"来读。
|
||||
|
||||
3. **剩下的 2 次分配是 API 形状的下限。** 一次是返回的 `R` 包装对象(24 字节),
|
||||
2. **剩下的 2 次分配是 API 形状的下限。** 一次是返回的 `R` 包装对象(24 字节),
|
||||
一次是可变参数切片经接口调用时的逃逸。想彻底避开,用 `Get[T]`。
|
||||
|
||||
---
|
||||
|
||||
@@ -116,11 +116,11 @@ func fromReflect(rv reflect.Value, writable bool) (rfx, bool) {
|
||||
}
|
||||
|
||||
// step 沿路径前进一段。
|
||||
//
|
||||
// 要求 v 已经 normalize 过, 且 Kind 不是 Map —— 两件事都由 walk 保证:
|
||||
// map 会被 walk 拦下来改走 walkReflect, 不会到这里。
|
||||
func (v rfx) step(seg string) (rfx, bool) {
|
||||
cur, ok := v.normalize()
|
||||
if !ok {
|
||||
return rfx{}, false
|
||||
}
|
||||
cur := v
|
||||
|
||||
switch cur.td.Kind {
|
||||
case reflect.Struct:
|
||||
@@ -163,19 +163,8 @@ func (v rfx) step(seg string) (rfx, bool) {
|
||||
}
|
||||
return rfx{td: cur.td.Elem, ptr: p, writable: cur.writable}, true
|
||||
|
||||
case reflect.Map:
|
||||
// map 没有稳定布局, 退回 reflect
|
||||
m := valueAt(cur.td, cur.ptr)
|
||||
if m.IsNil() {
|
||||
return rfx{}, false
|
||||
}
|
||||
mv := tryMapFieldValue(m, seg)
|
||||
if !mv.IsValid() {
|
||||
return rfx{}, false
|
||||
}
|
||||
return fromReflect(mv, false)
|
||||
|
||||
default:
|
||||
// 含 Map —— walk 已经把它拦到 walkReflect 去了, 走不到这里
|
||||
return rfx{}, false
|
||||
}
|
||||
}
|
||||
@@ -188,8 +177,35 @@ func (v rfx) walk(it pathIter) (rfx, bool) {
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
var ok bool
|
||||
cur, ok = cur.step(seg)
|
||||
norm, ok := cur.normalize()
|
||||
if !ok {
|
||||
return rfx{}, false
|
||||
}
|
||||
|
||||
// 命中 map: 用 reflect 连续走完**这一段 map**, 只在离开时装箱一次。
|
||||
//
|
||||
// 两头都要顾: 逐跳走 step 的话每跳都要 boxCopy(map 元素不可寻址,
|
||||
// 而 rfx 的表示需要一个地址), 连续多层 map 会累积多次堆拷贝; 但如果
|
||||
// 一路 reflect 走到底, map 后面的 struct 跳就丢了偏移量快路径,
|
||||
// 退回 FieldByName。所以只在 map 段内用 reflect, 一离开就切回去。
|
||||
if norm.td.Kind == reflect.Map {
|
||||
rv, pending, exhausted := walkMapRun(valueAt(norm.td, norm.ptr), seg, &it)
|
||||
if exhausted {
|
||||
return fromReflect(rv, false)
|
||||
}
|
||||
// 路径还没走完但已经不是 map 了: 装箱一次, 回到快路径继续。
|
||||
// pending 是离开 map 段之后的第一跳, walkMapRun 没有消费它。
|
||||
cur, ok = fromReflect(rv, false)
|
||||
if !ok {
|
||||
return rfx{}, false
|
||||
}
|
||||
if cur, ok = cur.step(pending); !ok {
|
||||
return rfx{}, false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
cur, ok = norm.step(seg)
|
||||
if !ok {
|
||||
return rfx{}, false
|
||||
}
|
||||
@@ -197,6 +213,38 @@ func (v rfx) walk(it pathIter) (rfx, bool) {
|
||||
return cur.normalize()
|
||||
}
|
||||
|
||||
// walkMapRun 用 reflect 连续走完一段 map: 从 seg 这一跳开始, 只要落点仍是
|
||||
// map 就继续吃下一段, 一旦不是 map(或路径走完)就返回。
|
||||
//
|
||||
// exhausted 为 false 时表示路径还没走完但已经离开 map 段, 此时 pending 是
|
||||
// 尚未消费的下一跳; 调用方应当把返回值装箱一次, 切回基于偏移量的快路径 ——
|
||||
// map 后面如果跟着 struct, 那些跳仍然值得走快路径。
|
||||
//
|
||||
// 注意 pending 是**返回**给调用方的, 而不是通过 it 指针回退。
|
||||
// 写回(*it = save)会让逃逸分析认为 it 指向的内容可能逃逸, 进而把 Get 的
|
||||
// 可变参数切片也判定为逃逸 —— 那会波及所有路径, 连纯 struct 的零分配快路径
|
||||
// 都跟着退化成 1 次分配。
|
||||
func walkMapRun(v reflect.Value, seg string, it *pathIter) (_ reflect.Value, pending string, exhausted bool) {
|
||||
for {
|
||||
if !v.IsValid() || v.Kind() != reflect.Map {
|
||||
return reflect.Value{}, "", true
|
||||
}
|
||||
v = derefWithAccessor(tryMapFieldValue(v, seg))
|
||||
if !v.IsValid() {
|
||||
return reflect.Value{}, "", true
|
||||
}
|
||||
|
||||
next, more := it.next()
|
||||
if !more {
|
||||
return v, "", true
|
||||
}
|
||||
if v.Kind() != reflect.Map {
|
||||
return v, next, false
|
||||
}
|
||||
seg = next
|
||||
}
|
||||
}
|
||||
|
||||
// boxed 把栈上的遍历结果装箱成返回给调用方的 *rfx(唯一一次分配)。
|
||||
func boxed(v rfx, ok bool) R {
|
||||
if !ok {
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
package reflux
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// walk 命中 map 之后会切到 walkReflect 走完剩余路径, 只在最后装箱一次
|
||||
// (原来是逐跳 boxCopy)。这一组守的是"切换之后行为完全不变"。
|
||||
//
|
||||
// 重点覆盖 map 之后的各种后继类型: struct / 指针 / 切片 / 数组 / 再一层 map /
|
||||
// interface / Accessor, 以及各自的失败形态。
|
||||
|
||||
type mwInner struct {
|
||||
Name string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
type mwDoc struct {
|
||||
Meta map[string]any
|
||||
Objs map[string]mwInner
|
||||
Ptrs map[string]*mwInner
|
||||
Nested map[string]map[string]any
|
||||
Arrs map[string][3]int
|
||||
Ifaces map[string]any
|
||||
Deep map[string]any
|
||||
}
|
||||
|
||||
func newMWDoc() *mwDoc {
|
||||
return &mwDoc{
|
||||
Meta: map[string]any{"s": "v", "n": 42, "b": true},
|
||||
Objs: map[string]mwInner{"o": {Name: "inner", Tags: []string{"t0", "t1"}}},
|
||||
Ptrs: map[string]*mwInner{"p": {Name: "ptr", Tags: []string{"p0"}}},
|
||||
Nested: map[string]map[string]any{
|
||||
"lv1": {"lv2": "deep"},
|
||||
},
|
||||
Arrs: map[string][3]int{"a": {10, 20, 30}},
|
||||
Ifaces: map[string]any{"i": mwInner{Name: "iface"}},
|
||||
Deep: map[string]any{
|
||||
"a": map[string]any{"b": map[string]any{"c": map[string]any{"leaf": "bottom"}}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapWalkMatchesLegacy(t *testing.T) {
|
||||
paths := [][]string{
|
||||
// map -> 标量
|
||||
{"Meta", "s"}, {"Meta", "n"}, {"Meta", "b"},
|
||||
// map -> struct -> 字段
|
||||
{"Objs", "o"}, {"Objs", "o", "Name"},
|
||||
// map -> struct -> 切片 -> 元素
|
||||
{"Objs", "o", "Tags"}, {"Objs", "o", "Tags", "1"},
|
||||
// map -> 指针 -> 字段
|
||||
{"Ptrs", "p"}, {"Ptrs", "p", "Name"}, {"Ptrs", "p", "Tags", "0"},
|
||||
// map -> map -> 键
|
||||
{"Nested", "lv1"}, {"Nested", "lv1", "lv2"},
|
||||
// map -> 数组 -> 下标
|
||||
{"Arrs", "a"}, {"Arrs", "a", "2"},
|
||||
// map -> interface -> struct -> 字段
|
||||
{"Ifaces", "i"}, {"Ifaces", "i", "Name"},
|
||||
// 四层 map 链
|
||||
{"Deep", "a"}, {"Deep", "a", "b"}, {"Deep", "a", "b", "c"},
|
||||
{"Deep", "a", "b", "c", "leaf"},
|
||||
{"Deep.a.b.c.leaf"},
|
||||
// 失败形态
|
||||
{"Meta", "nope"}, {"Objs", "o", "NoField"}, {"Objs", "nokey", "Name"},
|
||||
{"Objs", "o", "Tags", "99"}, {"Arrs", "a", "9"},
|
||||
{"Meta", "s", "further"}, {"Ptrs", "nokey", "Name"},
|
||||
{"Deep", "a", "b", "nope", "leaf"},
|
||||
}
|
||||
|
||||
ops := []struct {
|
||||
name string
|
||||
f func(R) any
|
||||
}{
|
||||
{"String", func(r R) any { return r.String() }},
|
||||
{"Int", func(r R) any { return r.Int() }},
|
||||
{"Any", func(r R) any { return fmt.Sprintf("%v", r.Any()) }},
|
||||
{"Exists", func(r R) any { return r.Exists() }},
|
||||
{"RawKind", func(r R) any { return r.Raw().Kind().String() }},
|
||||
{"Keys", func(r R) any { return fmt.Sprintf("%v", r.Keys()) }},
|
||||
{"JSON", func(r R) any { b, e := json.Marshal(r); return fmt.Sprintf("%s/%v", b, e) }},
|
||||
{"Array长度", func(r R) any { return len(r.Array()) }},
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
for _, op := range ops {
|
||||
t.Run(fmt.Sprintf("%v/%s", p, op.name), func(t *testing.T) {
|
||||
want := call(newLegacy(newMWDoc()).Get(p...), op.f)
|
||||
got := call(New(newMWDoc()).Get(p...), op.f)
|
||||
if want != got {
|
||||
t.Fatalf("行为不一致\n 旧 = %s\n 新 = %s", want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 经 map 的写入也要与旧实现一致(含写完之后整体数据的比对)
|
||||
func TestMapWalkWritesMatchLegacy(t *testing.T) {
|
||||
writes := []struct {
|
||||
path string
|
||||
val any
|
||||
}{
|
||||
{"Meta.s", "changed"},
|
||||
{"Meta.n", 7},
|
||||
{"Meta.new", "added"},
|
||||
{"Objs.o.Name", "changed"},
|
||||
{"Objs.o.Tags.0", "changed"},
|
||||
{"Ptrs.p.Name", "changed"},
|
||||
{"Ptrs.p.Tags.0", "changed"},
|
||||
{"Nested.lv1.lv2", "changed"},
|
||||
{"Deep.a.b.c.leaf", "changed"},
|
||||
{"Objs.o.NoField", "x"},
|
||||
{"Objs.nokey.Name", "x"},
|
||||
{"Meta.s.deeper", "x"},
|
||||
}
|
||||
for _, w := range writes {
|
||||
t.Run(w.path, func(t *testing.T) {
|
||||
dOld, dNew := newMWDoc(), newMWDoc()
|
||||
rOld, rNew := newLegacy(dOld), New(dNew)
|
||||
|
||||
resOld := call(rOld, func(r R) any { r.Set(w.path, w.val); return "ok" })
|
||||
resNew := call(rNew, func(r R) any { r.Set(w.path, w.val); return "ok" })
|
||||
if resOld != resNew {
|
||||
t.Fatalf("Set 行为不一致\n 旧 = %s\n 新 = %s", resOld, resNew)
|
||||
}
|
||||
|
||||
bOld, _ := json.Marshal(dOld)
|
||||
bNew, _ := json.Marshal(dNew)
|
||||
if string(bOld) != string(bNew) {
|
||||
t.Fatalf("写入后数据不一致\n 旧 = %s\n 新 = %s", bOld, bNew)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 根就是 map 的情况(没有任何 struct 跳)
|
||||
func TestMapWalkRootIsMap(t *testing.T) {
|
||||
mk := func() map[string]any {
|
||||
return map[string]any{
|
||||
"a": map[string]any{"b": map[string]any{"leaf": "x", "n": 1}},
|
||||
"s": "top",
|
||||
}
|
||||
}
|
||||
for _, p := range [][]string{
|
||||
{"s"}, {"a"}, {"a", "b"}, {"a", "b", "leaf"}, {"a", "b", "n"},
|
||||
{"a.b.leaf"}, {"a", "nope"}, {"a", "b", "leaf", "further"},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%v", p), func(t *testing.T) {
|
||||
mo, mn := mk(), mk()
|
||||
want := call(newLegacy(&mo).Get(p...), func(r R) any { return r.String() })
|
||||
got := call(New(&mn).Get(p...), func(r R) any { return r.String() })
|
||||
if want != got {
|
||||
t.Fatalf("旧 = %s, 新 = %s", want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// walk 命中 map 后只连续走完**这一段** map, 一离开就装箱切回快路径。
|
||||
// 交界处最容易出错: 离开 map 段的那一跳如果被 walkMapRun 吃掉又没交还,
|
||||
// 路径就会少走一跳且不报错。这一组专门压交界。
|
||||
func TestMapRunBoundary(t *testing.T) {
|
||||
type leaf struct{ V string }
|
||||
type mid struct {
|
||||
L leaf
|
||||
M map[string]any
|
||||
}
|
||||
type doc struct {
|
||||
M1 map[string]any
|
||||
M2 map[string]map[string]any
|
||||
}
|
||||
|
||||
mk := func() *doc {
|
||||
return &doc{
|
||||
M1: map[string]any{
|
||||
"s": mid{L: leaf{V: "a"}, M: map[string]any{"k": "in-mid"}},
|
||||
"sl": []any{mid{L: leaf{V: "in-slice"}}},
|
||||
},
|
||||
M2: map[string]map[string]any{
|
||||
"x": {"s": mid{L: leaf{V: "b"}}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
paths := [][]string{
|
||||
// map 之后紧跟 struct, 再往下走(交界在第 2 跳)
|
||||
{"M1", "s"}, {"M1", "s", "L"}, {"M1", "s", "L", "V"},
|
||||
// map -> struct -> map(离开又进入)
|
||||
{"M1", "s", "M"}, {"M1", "s", "M", "k"},
|
||||
// map -> map -> struct -> struct(连续两跳 map 后才离开)
|
||||
{"M2", "x"}, {"M2", "x", "s"}, {"M2", "x", "s", "L"}, {"M2", "x", "s", "L", "V"},
|
||||
// map -> slice -> struct
|
||||
{"M1", "sl"}, {"M1", "sl", "0"}, {"M1", "sl", "0", "L", "V"},
|
||||
// 点号写法应当与多参数完全一致
|
||||
{"M2.x.s.L.V"}, {"M1.s.M.k"},
|
||||
// 交界处的失败形态
|
||||
{"M1", "s", "NoField"}, {"M1", "s", "L", "NoField"},
|
||||
{"M2", "x", "s", "L", "V", "further"},
|
||||
{"M1", "nokey", "L", "V"}, {"M2", "nokey", "s", "L", "V"},
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
t.Run(fmt.Sprintf("%v", p), func(t *testing.T) {
|
||||
want := call(newLegacy(mk()).Get(p...), func(r R) any { return r.String() })
|
||||
got := call(New(mk()).Get(p...), func(r R) any { return r.String() })
|
||||
if want != got {
|
||||
t.Fatalf("旧 = %s, 新 = %s", want, got)
|
||||
}
|
||||
// 再比一次 Exists, 防止"取到空值"和"路径不存在"被混为一谈
|
||||
we := newLegacy(mk()).Get(p...).Exists()
|
||||
ge := New(mk()).Get(p...).Exists()
|
||||
if we != ge {
|
||||
t.Fatalf("Exists 不一致: 旧 = %v, 新 = %v", we, ge)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user