From ba0fd4b3bc0b40592881678f8ce337f93046ac48 Mon Sep 17 00:00:00 2001 From: what Date: Mon, 31 Aug 2026 11:14:03 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20Keys()=20=E5=8E=9F=E7=94=9F=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=20map=20=E5=88=86=E6=94=AF,=20=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2=E9=94=AE=E4=B8=8D=E5=86=8D=E7=BB=95=20fmt.Sprint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keys() 的 struct 分支早就是原生的了, map 分支一直委托给 refx, 而那边用的是 fmt.Sprint(k.Interface()) —— 要先把 key 装箱成 interface 再走一遍格式化。 对字符串键完全没必要。 Keys() 3 个字符串键的 map 193.3ns / 9 allocs -> 119.7ns / 5 allocs 1.61x 非字符串键仍旧走 fmt.Sprint: k.String() 对非字符串 Value 返回的是 "" 这种占位串而不是键本身, 无脑替换会静默给出错误的键名。 按 m.Type().Key().Kind() 分支处理。 顺带把 normalize() 提到开头, 这样经指针或 interface 拿到的 map/struct 也能 走原生分支, 而不是掉进 slow()。 测试期间发现并修掉一处自己引入的差异: nil map 上旧实现返回 []string{}, 我最初加的 IsNil 提前返回给出的是 nil。两者在 reflect.DeepEqual 和 JSON 序列化(`[]` vs `null`)上不同。MapKeys() 对 nil map 本来就返回空切片而不是 panic, 那个提前返回是多余的。 测试覆盖 11 种输入与旧实现对拍(string/具名 string/int/int64/float/bool 键、 空 map、nil map、struct、切片、嵌套 map), 外加经路径取到的 map、 interface 包着的 map、指针指向的 map, 以及非字符串键必须是真实键值的断言。 map 迭代顺序随机, 比较前排序。 --- rfx.go | 37 +++++++++++++++++--- rfx_keys_test.go | 87 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 rfx_keys_test.go diff --git a/rfx.go b/rfx.go index 96b9551..0bf4a5f 100644 --- a/rfx.go +++ b/rfx.go @@ -651,13 +651,42 @@ func (v *rfx) Keys() []string { if !v.valid() { return nil } - if v.td.Kind == reflect.Struct { - out := make([]string, len(v.td.fields)) - for i := range v.td.fields { - out[i] = v.td.fields[i].Name + cur, ok := (*v).normalize() + if !ok { + return nil + } + + switch cur.td.Kind { + case reflect.Struct: + out := make([]string, len(cur.td.fields)) + for i := range cur.td.fields { + out[i] = cur.td.fields[i].Name + } + return out + + case reflect.Map: + // 不必判 IsNil: MapKeys() 对 nil map 返回空切片而不是 panic, + // 于是这里得到 []string{} —— 与旧实现一致。提前返回 nil 会让 + // DeepEqual 和 JSON 序列化(`[]` vs `null`)出现差异。 + m := valueAt(cur.td, cur.ptr) + keys := m.MapKeys() + out := make([]string, len(keys)) + // 字符串键直接取, 不必绕 fmt.Sprint —— 后者要先把 key 装箱成 + // interface 再走一遍格式化, 实测 3 个键就差一倍。 + // 非字符串键仍旧交给 fmt.Sprint: k.String() 对非字符串 Value 返回的是 + // "" 这种占位串, 不是键本身。 + if m.Type().Key().Kind() == reflect.String { + for i, k := range keys { + out[i] = k.String() + } + } else { + for i, k := range keys { + out[i] = fmt.Sprint(k.Interface()) + } } return out } + return v.slow().Keys() } diff --git a/rfx_keys_test.go b/rfx_keys_test.go new file mode 100644 index 0000000..631d24e --- /dev/null +++ b/rfx_keys_test.go @@ -0,0 +1,87 @@ +package reflux + +import ( + "reflect" + "sort" + "testing" +) + +// Keys() 的 map 分支现在按键类型分支: 字符串键直接取, 其余仍走 fmt.Sprint。 +// 这一组守的是"分支之后行为不变" —— 尤其是非字符串键不能退化成 +// k.String() 的 "" 占位串。 + +type keyStr string + +func TestKeysMatchesLegacy(t *testing.T) { + type inner struct{ A, B string } + + cases := []struct { + name string + val func() any + }{ + {"string 键 map", func() any { return &map[string]int{"b": 2, "a": 1, "c": 3} }}, + {"具名 string 键 map", func() any { return &map[keyStr]int{"b": 2, "a": 1} }}, + {"int 键 map", func() any { return &map[int]string{2: "b", 1: "a", 10: "j"} }}, + {"int64 键 map", func() any { return &map[int64]string{7: "x"} }}, + {"float 键 map", func() any { return &map[float64]string{1.5: "x"} }}, + {"bool 键 map", func() any { return &map[bool]string{true: "t", false: "f"} }}, + {"空 map", func() any { return &map[string]int{} }}, + {"nil map", func() any { var m map[string]int; return &m }}, + {"struct", func() any { return &inner{} }}, + {"切片(两边都不支持)", func() any { return &[]string{"a"} }}, + {"map 里的 map", func() any { return &map[string]any{"sub": map[string]any{"x": 1}} }}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + want := newLegacy(c.val()).Keys() + got := New(c.val()).Keys() + // map 迭代顺序随机, 比较前排序 + sort.Strings(want) + sort.Strings(got) + if !reflect.DeepEqual(want, got) { + t.Fatalf("Keys() 不一致\n 旧 = %#v\n 新 = %#v", want, got) + } + }) + } +} + +// 经路径取到的 map / interface 包着的 map 也要正确 +func TestKeysThroughPath(t *testing.T) { + doc := &struct { + Meta map[string]string + Any any + Ptr *map[string]string + }{ + Meta: map[string]string{"k1": "v", "k2": "v"}, + Any: map[string]string{"a1": "v"}, + } + m := map[string]string{"p1": "v"} + doc.Ptr = &m + + r, l := New(doc), newLegacy(doc) + for _, p := range []string{"Meta", "Any", "Ptr"} { + t.Run(p, func(t *testing.T) { + want, got := l.Get(p).Keys(), r.Get(p).Keys() + sort.Strings(want) + sort.Strings(got) + if !reflect.DeepEqual(want, got) { + t.Fatalf("Get(%q).Keys() 不一致\n 旧 = %#v\n 新 = %#v", p, want, got) + } + if len(got) == 0 { + t.Fatalf("Get(%q).Keys() 不该为空", p) + } + }) + } +} + +// 非字符串键必须是键本身的值, 不能是 "" 这种占位串 +func TestKeysNonStringKeysAreReal(t *testing.T) { + got := New(&map[int]string{42: "x"}).Keys() + if len(got) != 1 || got[0] != "42" { + t.Fatalf("int 键的 Keys() = %#v, 期望 [\"42\"]", got) + } + if got := New(&map[bool]string{true: "x"}).Keys(); len(got) != 1 || got[0] != "true" { + t.Fatalf("bool 键的 Keys() = %#v, 期望 [\"true\"]", got) + } +}