reflect.TypeOf(fn).NumIn() 慢是因为每次调用都触发完整类型元数据解析并新建 reflect.Type 实例,无法复用底层 *rtype;应缓存 reflect.Type 的 uintptr 地址而非 reflect.Value。

为什么 reflect.TypeOf(fn).NumIn() 每次调用都慢
不是因为 reflect.TypeOf 本身重,而是它触发了运行时类型元数据的完整解析:包括方法表遍历、参数类型递归展开、reflect.Type 实例构造。哪怕函数签名完全不变,每次调用都新建 reflect.Type 对象,且无法复用底层 *rtype 的缓存(Go 不暴露该指针给用户)。高频路径下,单次开销约 80–200ns,累积起来远超函数体执行时间。
缓存什么才真正有效:只缓存 reflect.Type,别碰 reflect.Value
reflect.Type 是只读、全局单例,同一函数类型多次调用 reflect.TypeOf(fn) 返回的 reflect.Type 实例,其底层 *rtype 地址恒定。可安全用 uintptr(unsafe.Pointer(t)) 作 map key 缓存签名信息;而 reflect.Value 每次都新分配,缓存它等于白干。
- 正确做法:
var sigCache = sync.Map{} // key: uintptr, value: *funcSig t := reflect.TypeOf(fn) key := uintptr(unsafe.Pointer(t)) if cached, ok := sigCache.Load(key); ok { return cached.(*funcSig) } - 错误做法:缓存
reflect.ValueOf(fn).Type()或更糟——缓存reflect.ValueOf(fn)本身 - 注意:
fn必须是函数字面量或变量,不能是接口类型(如interface{}),否则reflect.TypeOf返回的是接口类型而非具体函数类型
绕过 reflect.TypeOf:用 go:generate 预生成签名检查代码
如果目标函数集固定(如 RPC 方法、HTTP 处理器集合),最彻底的降开销方式是在构建期把签名校验逻辑固化为普通 Go 代码。比如生成一个 validateHandler(fn interface{}) error,内部直接 switch 类型断言,不走反射。
- CI 中必须强制校验:
go generate && git diff --quiet || (echo "signature check out of date" && exit 1) - 生成逻辑需覆盖:
NumIn()、In(i)、NumOut()、Out(i)全部校验项 - 避免手写 tag 解析,用
github.com/iancoleman/strcase统一处理大小写
最容易被忽略的点:校验失败 panic 的成本比校验本身高得多
很多人省略签名检查,寄希望于 reflect.Value.Call 报错时再兜底。但 reflect: Call using zero Value argument 这类 panic 触发后需构造 stack trace、查找 panic handler、恢复 goroutine 栈——实际耗时是正常校验的 5–10 倍。宁可在注册期多花 100ns 做一次 t.NumIn() == 2 判断,也别赌运行时不出错。
立即学习“go语言免费学习笔记(深入)”;



















