【结论】同意,但断言不该放在调用点——m.Func.Interface() 本身就是一次装箱,每次调用都做一遍,等于把 reflectcall 省下的钱又交给 packEface 了。断言应该在建表时做掉。
【展开】一级表存 map[string]any,构建期遍历 NumMethod() 时做一次 m.Func.Interface() 并断言/存成具体函数值;调用点只写 f := tbl["M"].(func(*T, int) error),这是接口→具体类型的断言,纯指针比较,无分配。这样每次调用只剩一次 map 查找 + 一次断言 + 普通函数调用,确实能摸到直接调用的量级。反过来说,如果表里存的是 reflect.Value、调用点才 Interface(),那每次都在 packEface,收益直接对折,别在这里省事。
签名杂的场合,一个 switch f := entry.(type) 覆盖 3–4 种常见签名比一条一分支划算;超过这个数就该考虑 codegen,或者按 xiaoxin 那档收敛成接口(v.Interface().(iface).M() 只装一次箱,可维护性还高)。typing 断言分支写多了其实是把反射的复杂度搬进了业务代码。
一级表我更倾向 atomic.Pointer + copy-on-write 而不是 sync.Map:读路径零锁零分配,写时复制整表,Type 数量本来就不大,复制成本可忽略;sync.Map 在 key 是接口时也带装箱。阈值降级建议直接「超上限就不插,走慢路径」,别用计数器,否则过峰回落时插不回去还抖。
【结尾】&v 逃逸那条补个折中:只读方法用值接收者、从 T 取 Func,就能避免 &v;另外断言分支里顺手判下 receiver 是不是 nil 指针——Func 是 valid 的,panic 会落在方法体里,比落在反射里难定位得多。

正式会员
认证极客









