★ reflect 的基本用法是什么?性能代价有多大?
一句话结论:reflect 让你在运行时检查并操作任意值的类型和值:TypeOf 拿类型、ValueOf 拿值,通过 Kind() 分派、Set/SetXxx 写值(可写需传指针)、遍历结构体字段与 tag;代价是失去编译期类型检查、无法内联、伴随装箱与逃逸,热点路径比直接调用慢一个数量级甚至更多。
基本用法
- reflect.TypeOf(x):返回类型信息,可遍历字段/方法、读 tag。
- reflect.ValueOf(x):包装运行时的值;要修改时传指针并 ValueOf(&x).Elem(),先检查 CanSet。
- Kind():返回底层种类(Struct/Slice/Map/Int 等),做分派避免接口断言地狱。
- 读 tag:t.Field(i).Tag.Get("json"),ORM、校验器等框架常用。
type User struct {
Name string `json:"name"`
Age int `json:"age,omitempty"`
}
func printInfo(v any) {
t := reflect.TypeOf(v)
val := reflect.ValueOf(v)
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
fmt.Printf("%s kind=%v tag=%q value=%v\n",
f.Name, f.Type.Kind(), f.Tag, val.Field(i).Interface())
}
}
性能代价从哪来
- 编译期无法静态分派,全部走运行时类型判断与间接调用,无法内联。
- 接口装箱、反射产生的中间对象与逃逸推高堆分配,加重 GC。
- Value.Call / Interface() 有额外开销,Set 会做运行时类型检查。
- 实测:纯反射调用通常是直接调用的数倍到数十倍(视操作而定)。
工程上怎么规避
| 方案 | 适用场景 |
|---|---|
| 泛型(Go 1.18+) | 类型参数化算法与容器,编译期展开,无反射开销 |
| 代码生成 | 强类型要求加性能敏感的重复代码 |
| 反射结果缓存 | 结构体元信息只解析一次,之后查缓存 |
| 热点函数手写特例 | 只对最热的一两种类型走专门实现 |
常见追问 / 记忆点
- 追问:反射能修改未导出字段吗?——基本不能:未导出字段 CanSet 为 false,硬来要 unsafe。
- 追问:为什么 json/gorm 这类库必须用反射?——它们在运行时面对任意用户类型,类型系统给不了“按运行时类型遍历字段”的能力。
- 记忆点:框架里反射可接受,业务热点路径尽量不用;tag 等一次性解析要缓存。