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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user