perf: Keys() 原生实现 map 分支, 字符串键不再绕 fmt.Sprint

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 返回的是
"<int 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 迭代顺序随机, 比较前排序。
This commit is contained in:
2026-08-31 11:14:24 +08:00
parent a2c0c9783e
commit ba0fd4b3bc
2 changed files with 120 additions and 4 deletions
+33 -4
View File
@@ -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 返回的是
// "<int 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()
}