百度百科给的反射定义:“反射是一种计算机处理方式。有程序可以访问、检测和修改它本身状态或行为的这种能力。”我是觉着这个定义有点晦涩难懂,至少我在学Java的时候没太搞明白是什么意思。
struct TestStrcut type{
IsField int `json:"is_field"`// 这个是IsField字段,类型是int,tag内容为is_field
}
为了让大家对反射先有个认识,由此我先提问一个小问题,我们在编写golang程序时,如果想获取到某个结构体的字段名或者类型名,可以获取到吗?
答:在程序中,貌似我们只能读取和修改实例的值,并不能读到他是什么类型(int、struct等类型)和字段名(上面代码中的IsField这个字段名)。如果我们真的有这个需求呢,想读到实例本身的元数据呢,我们需要借助于反射的特性,通过反射我们可以获取到实例本身的元数据信息(字段类型、字段名、字段tag等信息)。
类型元数据信息:元数据信息的意思指,类型本身的一些信息,字段名,tag名,类型名等信息。元数据信息存储在可执行文件的哪个位置,就不进行阐述了(目前我也不太清楚,哈哈,大家见谅啊),肯定不在堆栈上,也不在data和bss区域上。
看到这里,我们应该清楚反射最常用的一个功能,运行时获取实例的类型,这个特性一般用于什么场景呢?
答:我现在能想到的有两个例子:1.将数据序列化与反序列化(将结构体实例转化为json文本)2.数据库表和结构体相互映射。从这里可以看到,反射还是很有用的。大家也可以思考一下,反射应用的例子。
现在我对反射的特性作一个比较全面的描述,一共有以下几个作用:
- 访问任意一个实例所属的类型以及该类型的元数据信息。
- 构造任意一个结构的实例。意思就是指利用反射可以在运行程序时生成结构体类型。
- 访问任意一个实例所具有的字段内容和方法,并且可以调用该实例的方法
反射性能分析
大家都或多或少听说过反射性能偏低,使用反射要比正常调用要低几倍到数十倍,不知道大家有没有思考过反射性能都低在哪些方面,我先做一个简单分析,通过反射在获取或者修改值内容时,多了几次内存引用,多绕了几次弯,肯定没有直接调用某个值来的迅速,这个是反射带来的固定性能损失,还有一方面的性能损失在于,结构体类型字段比较多时,要进行遍历匹配才能获取对应的内容。下面就根据反射具体示例来分析性能:
首先制定一个结构体,在后续写性能测试示例时会用得到,结构体如下:
// 测试用例
type TestReflectField struct {
A string
B int
C bool
D float64
}
// F 是一个无输入输出的方法
func (s *TestReflectField) F() {
_ = fmt.Sprint(0)
}
// F1 是一个有简单输入值的方法
func (s *TestReflectField) F1(a int) {
_ = fmt.Sprint(0)
}
// F2 是一个有复杂输入值的方法
func (s *TestReflectField) F2(a TestReflectField) {
_ = fmt.Sprint(0)
}
// F3 是一个接口作为输入值的方法
func (s *TestReflectField) F3(a interface{}) {
_ = fmt.Sprint(0)
}
// F4 是一个有多个输入值的方法
func (s *TestReflectField) F4(a, b, c, d, e, f int) {
_ = fmt.Sprint(0)
}
// F5 是一个有返回值的方法
func (s *TestReflectField) F5() int {
return 0
}
// TestInterface 定义一个接口
type TestInterface interface {
F()
}
测试结构体初始化
// 测试结构体初始化的反射性能
func Benchmark_Reflect_New(b *testing.B) {
var tf *TestReflectField
t := reflect.TypeOf(TestReflectField{})
for i := 0; i < b.N; i++ {
tf = reflect.New(t).Interface().(*TestReflectField)
}
_ = tf
}
// 测试结构体初始化的性能
func Benchmark_New(b *testing.B) {
var tf *TestReflectField
for i := 0; i < b.N; i++ {
tf = new(TestReflectField)
}
_ = tf
}
测试结果如下:
性能分析:
使用反射初始化结构体创建实例,比使用new创建实例性能要低两倍不到,性能损耗不算大,在可以接受的范围。
测试结构体字段赋值
测试正常修改字段内容和通过反射设置字段内容的性能对比,测试代码如下:
// 测试反射设置结构体字段的性能
func Benchmark_Reflect_Field(b *testing.B) {
var tf = new(TestReflectField)
temp := reflect.ValueOf(tf).Elem()
for i := 0; i < b.N; i++ {
temp.Field(1).SetInt(int64(1995))
}
_ = tf
}
// 测试反射设置结构体字段的性能
func Benchmark_Reflect_FieldByName(b *testing.B) {
var tf = new(TestReflectField)
temp := reflect.ValueOf(tf).Elem()
for i := 0; i < b.N; i++ {
temp.FieldByName("B").SetInt(int64(1995))
}
_ = tf
}
// 测试结构体字段设置的性能
func Benchmark_Field(b *testing.B) {
var tf = new(TestReflectField)
for i := 0; i < b.N; i++ {
tf.B = i
}
_ = tf
}
测试结果如下:
性能分析:
直接对实例变量进行赋值每次0.28ns,性能是通过反射操作实例变量的23倍左右。使用反射的两个测试案例都很慢,使用FieldByName("B")方法性能比使用Field(2)方法性能要低十倍左右,看代码的话我们会发现,FieldByName是通过遍历匹配所有的变量,来查询所需要的内容,所以性能要低上很多。建议:减少使用FieldByName方法。在需要使用反射进行成员变量访问的时候,尽可能的使用成员的序号。如果只知道成员变量的名称的时候,看具体代码的使用场景,如果可以在启动阶段或在频繁访问前,通过 Type.NumField 、Type.Field 和 StructField.Name 得到成员的序号,注意这里需要的是使用的是 reflect.Type 而不是 reflect.Value,通过 reflect.Value 是得不到字段名称的。
测试实例变量读取性能
测试通过实例变量直接读取数据和通过反射读取数据的性能,示例代码如下:
// 测试反射读取结构体字段值的性能
func Benchmark_Reflect_GetField(b *testing.B) {
var tf = new(TestReflectField)
var r int64
temp := reflect.ValueOf(tf).Elem()
for i := 0; i < b.N; i++ {
r = temp.Field(1).Int()
}
_ = tf
_ = r
}
// 测试反射读取结构体字段值的性能
func Benchmark_Reflect_GetFieldByName(b *testing.B) {
var tf = new(TestReflectField)
temp := reflect.ValueOf(tf).Elem()
var r int64
for i := 0; i < b.N; i++ {
r = temp.FieldByName("B").Int()
}
_ = tf
_ = r
}
// 测试结构体字段读取数据的性能
func Benchmark_GetField(b *testing.B) {
var tf = new(TestReflectField)
tf.B = 1995
var r int
for i := 0; i < b.N; i++ {
r = tf.B
}
_ = tf
_ = r
}
性能分析:
使用反射读取数据比普通读取数据满了16倍左右,这都是源于多了几次内存调用导致的性能下降。通过FieldByName("B")方法获取数据比通过序号Field(2)方法性能低了十几倍,原因上面已经阐述过了。建议如上,如果只知道字段名字的前提下,先提前通过 Type.NumField 、Type.Field 和 StructField.Name 得到成员的序号,然后调用Field()获取数据。
测试方法调用的性能
测试各种情形下的方法调用,无参数,有参数,有复杂参数,有返回值方法等情况,测试案例如下:
// 测试通过结构体访问方法性能
func BenchmarkMethod(b *testing.B) {
t := &TestReflectField{}
for i := 0; i < b.N; i++ {
t.F()
}
}
// 测试通过反射访问无参数方法性能
func BenchmarkReflectMethod(b *testing.B) {
v := reflect.ValueOf(&TestReflectField{})
for i := 0; i < b.N; i++ {
v.Method(0).Call(nil)
}
}
// 测试通过反射访问无参数方法性能
func BenchmarkReflectMethodByName(b *testing.B) {
v := reflect.ValueOf(&TestReflectField{})
for i := 0; i < b.N; i++ {
v.MethodByName("F").Call(nil)
}
}
// 测试通过反射访问有参数方法性能
func BenchmarkReflectMethod_WithArgs(b *testing.B) {
v := reflect.ValueOf(&TestReflectField{})
for i := 0; i < b.N; i++ {
v.Method(1).Call([]reflect.Value{reflect.ValueOf(i)})
}
}
// 测试通过反射访问结构体参数方法性能
func BenchmarkReflectMethod_WithArgs_Mul(b *testing.B) {
v := reflect.ValueOf(&TestReflectField{})
for i := 0; i < b.N; i++ {
v.Method(2).Call([]reflect.Value{reflect.ValueOf(TestReflectField{})})
}
}
// 测试通过反射访问接口参数方法性能
func BenchmarkReflectMethod_WithArgs_Interface(b *testing.B) {
v := reflect.ValueOf(&TestReflectField{})
for i := 0; i < b.N; i++ {
var tf TestInterface = &TestReflectField{}
v.Method(3).Call([]reflect.Value{reflect.ValueOf(tf)})
}
}
// 测试访问多参数方法性能
func BenchmarkMethod_WithManyArgs(b *testing.B) {
s := &TestReflectField{}
for i := 0; i < b.N; i++ {
s.F4(i, i, i, i, i, i)
}
}
// 测试通过反射访问多参数方法性能
func BenchmarkReflectMethod_WithManyArgs(b *testing.B) {
v := reflect.ValueOf(&TestReflectField{})
va := make([]reflect.Value, 0)
for i := 1; i <= 6; i++ {
va = append(va, reflect.ValueOf(i))
}
for i := 0; i < b.N; i++ {
v.Method(4).Call(va)
}
}
// 测试访问有返回值的方法性能
func BenchmarkMethod_WithResp(b *testing.B) {
s := &TestReflectField{}
for i := 0; i < b.N; i++ {
_ = s.F5()
}
}
// 测试通过反射访问有返回值的方法性能
func BenchmarkReflectMethod_WithResp(b *testing.B) {
v := reflect.ValueOf(&TestReflectField{})
for i := 0; i < b.N; i++ {
_ = v.Method(5).Call(nil)[0].Int()
}
}
测试结果如下:
性能分析:
经过前几次性能测试,不用看测试结果我们也大概能猜出结果是什么样子,通过反射调用方法肯定比普通调用方法慢很多。不考虑方法执行内容的前提下,反射调用方法比直接调用慢5~10倍,如果考虑到方法本身就有复杂的执行过程,这个性能差距会被缩小。这几个不同情况的反射也有差别,性能从高到低如下:无参数方法>有参数方法≈接口调用有参数方法>复杂参数方法>多参数方法。
总结
使用反射必定会导致性能下降,但是反射是一个强有力的工具,可以解决我们平时的很多问题,比如数据库映射、数据序列化、代码生成场景。在使用反射的时候,我们需要避免一些性能过低的操作,例如使用FieldByName()和MethodByName()方法,如果必须使用这些方法的时候,我们可以预先通过字段名或者方法名获取到对应的字段序号,然后使用性能较高的反射操作,以此提升使用反射的性能。
参考
1.golang 1.12 工具库
2.公司内部技术博客-Golang 反射性能和优化
源码:见下方资源,如果可以显示的话