nummethod() 每次调用都慢,因其不查缓存而是线性扫描类型元数据统计导出方法数;建议用 sync.map 缓存 uintptr(unsafe.pointer(t)) → int 结果,避免重复反射开销。

reflect.TypeOf(v).NumMethod() 为什么每次调用都慢
因为 NumMethod() 不是查缓存,而是遍历类型元数据重新统计导出方法个数。Go 的 reflect.Type 虽然底层复用 abi.Type,但每次调用 NumMethod() 都会触发 runtime 层的线性扫描——哪怕你刚调过一模一样的类型。
实操建议:
- 用
sync.Map缓存结果:键为uintptr(unsafe.Pointer(t)),值为t.NumMethod()返回的整数 - 别缓存
reflect.ValueOf(v).Type().NumMethod(),因为reflect.ValueOf()每次新建reflect.Value实例,无法复用 - 若结构体嵌套深或方法多(>50 个),这个扫描开销会明显上升,不是常数时间
Method(i) 返回的 reflect.Method 不可直接缓存为 map key
reflect.Method 是一个 struct,含 Name、PkgPath、Type、Func 等字段,其中 Func 是 reflect.Value 类型,每次调用 Method(i) 都新建,不能当 map key 用(会 panic 或永远不命中)。
常见错误现象:写 cache[methodName] = t.Method(i),结果每次查都是 miss;或者用 map[interface{}]reflect.Method,因底层 interface{} 包含不同地址而失效。
实操建议:
- 只缓存
t.Method(i).Name和t.Method(i).Func.Type()的签名哈希(如fmt.Sprintf("%s-%s", name, typ.String())) - 真正要复用的是
reflect.Method.Func对应的函数指针,可用unsafe提前算出并封装为闭包(见下节) - 避免在热路径上反复调用
t.Method(i),尤其i是变量时,CPU branch predictor 容易失效
MethodByName 查不到就 panic: call of reflect.Value.Call on zero Value
MethodByName 找不到方法时返回无效 reflect.Value,.IsValid() 为 false,但很多人直接 .Call() 导致 panic。这不是 bug,是设计使然——Go 反射不自动 fallback,也不报错提示。
实操建议:
- 必须显式检查:
if !method.IsValid() { return fmt.Errorf("no such method: %s", name) } - 大小写敏感:传
"SetName"却定义了"setname",一定失败;小写开头的方法反射不可见,MethodByName永远不返回它们 - 接收者不匹配:对值类型调用指针接收者方法(如
reflect.ValueOf(s).MethodByName("Foo"),而Foo是(*T) Foo()),也会!IsValid()
真正高频场景该绕开 reflect.Value.Call
reflect.Value.Call 的瓶颈不在“调用”本身,而在每次调用前强制做的两件事:receiver 可寻址性校验 + 所有参数逐个转换成 reflect.Value。单次开销 100–500ns,10k QPS 就吃掉 1–5ms CPU 时间。
实操建议:
- 预计算方法入口地址:用
unsafe读取abi.Type中的方法表偏移,再构造函数指针(Go 1.14+ ABI 稳定) - 把调用逻辑下沉为闭包:
var fn func(interface{}) error = makeInvoker(t, "Validate"),运行时只传实例,无反射开销 - 如果类型固定、数量可控(如 API 请求结构体),优先走代码生成,而不是“缓存反射结果”这种半吊子优化
最容易被忽略的一点:缓存 reflect.Method 没用,缓存 reflect.Value 更没用;真正值得预计算的,是方法在内存中的绝对地址和参数布局——这已经脱离了反射范畴,进入 unsafe 与 ABI 协作的边界。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











