应缓存reflect.Method而非reflect.Value,因前者只依赖类型和方法签名、地址恒定可复用,后者绑定具体实例无法复用;错误缓存Value开销大且无效,正确方式是用uintptr(unsafe.Pointer(t))作key缓存Method。

缓存 reflect.Method 而不是 reflect.Value
每次调用 reflect.ValueOf(&obj).MethodByName("Foo") 都会重新查找方法表、校验导出性、构造新 reflect.Value,开销固定在 50–200ns。但 reflect.Method 是只读结构体,同一类型+方法名返回的实例地址恒定,可安全缓存。
常见错误是缓存 reflect.Value(比如 cache["Foo"] = v.MethodByName("Foo")),这毫无意义——v 绑定的是具体实例,无法复用;而 reflect.Method 不依赖实例,只依赖类型和方法签名。
- 正确方式:用
reflect.TypeOf(&T{}).Elem()得到结构体Type,再用t.Method(i)遍历预热所有需调用的方法,存入map[uintptr]reflect.Method - 键推荐用
uintptr(unsafe.Pointer(t)),比字符串 key 快且无哈希开销 - 不要在请求路径里做
MethodByName,启动时或首次访问时完成缓存
避免 FieldByName 和 Call 前的重复校验
FieldByName 是线性遍历字段名字符串比对,100 字段就要比 100 次;Call 前又强制做 receiver 可寻址性检查和每个参数类型转换——这两步加起来占了整个反射调用 70% 以上耗时。
- 字段访问一律改用
FieldByIndex:提前把"ID"→[]int{0}映射存好,查表 O(1) -
Call前必须检查method.IsValid()和method.CanCall(),漏掉任一都会 panic,且错误信息不指明行号 - 参数切片
[]reflect.Value也建议预分配并复用,避免每次make触发 GC
高频热路径直接绕过 reflect.Call
哪怕缓存了 reflect.Method,只要还走 method.Call(args),就逃不开 receiver 校验和参数转换。Go 1.14+ 后,internal/abi.Type 布局稳定,可直接计算方法入口地址。
- 用
unsafe从类型信息中提取方法表偏移,再算出第 i 个方法的函数指针地址 - 封装为闭包:
func(v interface{}) { callFunc(funcPtr, v) },运行时只传指针,无反射开销 - 代价是失去类型安全:方法签名错一位(比如
int写成int64)会导致 runtime crash,不是 panic
CanSet 和指针接收者必须严格匹配
非指针值调用 reflect.ValueOf(obj) 得到的是副本,CanSet() 返回 false;想修改原值,必须传 &obj 并调 Elem()。这个约束常被忽略,导致 SetInt() 等操作直接 panic。
- 错误写法:
v := reflect.ValueOf(myStruct); v.FieldByName("ID").SetInt(1)→ panic: unaddressable value - 正确写法:
v := reflect.ValueOf(&myStruct).Elem(); v.FieldByName("ID").SetInt(1) - 更隐蔽的是:如果
myStruct本身是指针(var myStruct *MyStruct),直接reflect.ValueOf(myStruct)即可,再调Elem()才能拿到可寻址字段
真正难的不是让反射跑起来,而是判断“这里到底该不该用反射”——很多本可用泛型约束或接口断言解决的地方,硬套反射只会把性能瓶颈和 panic 风险埋得更深。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











