Go reflect包提供了运行时获取对象的类型和值的能力,它可以帮助我们实现代码的抽象和简化,实现动态的数据获取和方法调用, 提高开发效率和可读性, 也弥补Go在缺乏泛型的情况下对数据的统一处理能力。
通过reflect,我们可以实现获取对象类型、对象字段、对象方法的能力,获取struct的tag信息,动态创建对象,对象是否实现特定的接口,对象的转换、对象值的获取和设置、Select分支动态调用等功能, 看起来功能不错,但是大家也都知道一点: 使用reflect是有性能代价的!
测试
Java中的reflect的使用对性能也有影响, 但是和Java reflect不同, Java中不区分Type
和Value
类型的, 所以至少Java中我们可以预先将相应的reflect对象缓存起来,减少反射对性能的影响, 但是Go没办法预先缓存reflect, 因为Type
类型并不包含对象运行时的值,必须通过ValueOf
和运行时实例对象才能获取Value
对象。
对象的反射生成和获取都会增加额外的代码指令, 并且也会涉及interface{}
装箱/拆箱操作,中间还可能增加临时对象的生成,所以性能下降是肯定的,但是具体能下降多少呢,还是得数据来说话。
当然,不同的reflect使用的姿势, 以及对象类型的不同,都会多多少少影响性能的测试数据,我们就以一个普通的struct类型为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| package test import ( "reflect" "testing" ) type Student struct { Name string Age int Class string Score int } func BenchmarkReflect_New(b *testing.B) { var s *Student sv := reflect.TypeOf(Student{}) b.ResetTimer() for i := 0; i < b.N; i++ { sn := reflect.New(sv) s, _ = sn.Interface().(*Student) } _ = s } func BenchmarkDirect_New(b *testing.B) { var s *Student b.ResetTimer() for i := 0; i < b.N; i++ { s = new(Student) } _ = s } func BenchmarkReflect_Set(b *testing.B) { var s *Student sv := reflect.TypeOf(Student{}) b.ResetTimer() for i := 0; i < b.N; i++ { sn := reflect.New(sv) s = sn.Interface().(*Student) s.Name = "Jerry" s.Age = 18 s.Class = "20005" s.Score = 100 } } func BenchmarkReflect_SetFieldByName(b *testing.B) { sv := reflect.TypeOf(Student{}) b.ResetTimer() for i := 0; i < b.N; i++ { sn := reflect.New(sv).Elem() sn.FieldByName("Name").SetString("Jerry") sn.FieldByName("Age").SetInt(18) sn.FieldByName("Class").SetString("20005") sn.FieldByName("Score").SetInt(100) } } func BenchmarkReflect_SetFieldByIndex(b *testing.B) { sv := reflect.TypeOf(Student{}) b.ResetTimer() for i := 0; i < b.N; i++ { sn := reflect.New(sv).Elem() sn.Field(0).SetString("Jerry") sn.Field(1).SetInt(18) sn.Field(2).SetString("20005") sn.Field(3).SetInt(100) } } func BenchmarkDirect_Set(b *testing.B) { var s *Student b.ResetTimer() for i := 0; i < b.N; i++ { s = new(Student) s.Name = "Jerry" s.Age = 18 s.Class = "20005" s.Score = 100 } }
|
测试结果:
1 2 3 4 5 6 7
| BenchmarkReflect_New-4 20000000 70.0 ns/op 48 B/op 1 allocs/op BenchmarkDirect_New-4 30000000 45.6 ns/op 48 B/op 1 allocs/op BenchmarkReflect_Set-4 20000000 73.6 ns/op 48 B/op 1 allocs/op BenchmarkReflect_SetFieldByName-4 3000000 492 ns/op 80 B/op 5 allocs/op BenchmarkReflect_SetFieldByIndex-4 20000000 111 ns/op 48 B/op 1 allocs/op BenchmarkDirect_Set-4 30000000 43.1 ns/op 48 B/op 1 allocs/op
|
测试结果
我们进行了两种功能的测试:
对于对象的创建,通过反射生成对象需要 70
纳秒, 而直接new这个对象却只需要45.6
纳秒, 性能差别还是很大的。
对于字段的赋值,一共四个测试用例:
- Reflect_Set: 通过反射生成对象,并将这个对象转换成实际的对象,直接调用对象的字段进行赋值, 需要
73.6
纳秒
- Reflect_SetFieldByName: 通过反射生成对象,通过
FieldByName
进行赋值, 需要492
纳秒
- Reflect_SetFieldByIndex: 通过反射生成对象,通过
Field
进行赋值, 需要111
纳秒
- Direct_Set: 直接调用对象的字段进行赋值, 只需要
43.1
纳秒
Reflect_Set
和Direct_Set
性能的主要差别还是在于对象的生成,因为之后字段的赋值方法都是一样的,这也和对象创建的测试case的结果是一致的。
如果通过反射进行赋值,性能下降是很厉害的,耗时成倍的增长。比较有趣的是,FieldByName
方式赋值是Field
方式赋值的好几倍, 原因在于FieldByName
会有额外的循环进行字段的查找,虽然最终它还是调用Field
进行赋值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| func (v Value) FieldByName(name string) Value { v.mustBe(Struct) if f, ok := v.typ.FieldByName(name); ok { return v.FieldByIndex(f.Index) } return Value{} } func (v Value) FieldByIndex(index []int) Value { if len(index) == 1 { return v.Field(index[0]) } v.mustBe(Struct) for i, x := range index { if i > 0 { if v.Kind() == Ptr && v.typ.Elem().Kind() == Struct { if v.IsNil() { panic("reflect: indirection through nil pointer to embedded struct") } v = v.Elem() } } v = v.Field(x) } return v }
|
优化
从上面的测试结果看, 通过反射生成对象和字段赋值都会影响性能,但是通过反射的确确确实实能简化代码,为业务逻辑提供统一的代码, 比如标准库中json的编解码、rpc服务的注册和调用, 一些ORM框架比如gorm等,都是通过反射处理数据的,这是为了能处理通用的类型。
https://github.com/golang/go/blob/master/src/encoding/json/decode.go#L9461 2 3 4 5 6 7 8 9 10
| ...... case reflect.String: v.SetString(string(s)) case reflect.Interface: if v.NumMethod() == 0 { v.Set(reflect.ValueOf(string(s))) } else { d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) } ......
|
https://github.com/jinzhu/gorm/blob/master/scope.go#L4951 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| for fieldIndex, field := range selectFields { if field.DBName == column { if field.Field.Kind() == reflect.Ptr { values[index] = field.Field.Addr().Interface() } else { reflectValue := reflect.New(reflect.PtrTo(field.Struct.Type)) reflectValue.Elem().Set(field.Field.Addr()) values[index] = reflectValue.Interface() resetFields[index] = field } selectedColumnsMap[column] = offset + fieldIndex if field.IsNormal { break } } }
|
在我们追求高性能的场景的时候,我们可能需要尽量避免反射的调用, 比如对json数据的unmarshal, easyjson就通过生成器的方式,避免使用反射。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| func (v *Student) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} easyjson4a74e62dDecodeGitABCReflect(&r, v) return r.Error() } func (v *Student) UnmarshalEasyJSON(l *jlexer.Lexer) { easyjson4a74e62dDecodeGitABCReflect(l, v) } func easyjson4a74e62dDecodeGitABCReflect(in *jlexer.Lexer, out *Student) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { in.Consumed() } in.Skip() return } in.Delim('{') for !in.IsDelim('}') { key := in.UnsafeString() in.WantColon() if in.IsNull() { in.Skip() in.WantComma() continue } switch key { case "Name": out.Name = string(in.String()) case "Age": out.Age = int(in.Int()) case "Class": out.Class = string(in.String()) case "Score": out.Score = int(in.Int()) default: in.SkipRecursive() } in.WantComma() } in.Delim('}') if isTopLevel { in.Consumed() } }
|
其它的一些编解码库也提供了这种避免使用反射的方法来提高性能。
顺带测一下"装箱/拆箱"操作带来的性能影响
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| func DirectInvoke(s *Student) { s.Name = "Jerry" s.Age = 18 s.Class = "20005" s.Score = 100 } func InterfaceInvoke(i interface{}) { s := i.(*Student) s.Name = "Jerry" s.Age = 18 s.Class = "20005" s.Score = 100 } func BenchmarkDirectInvoke(b *testing.B) { s := new(Student) for i := 0; i < b.N; i++ { DirectInvoke(s) } _ = s } func BenchmarkInterfaceInvoke(b *testing.B) { s := new(Student) for i := 0; i < b.N; i++ { InterfaceInvoke(s) } _ = s }
|
测试结果:
1 2
| BenchmarkDirectInvoke-4 300000000 5.60 ns/op 0 B/op 0 allocs/op BenchmarkInterfaceInvoke-4 200000000 6.64 ns/op 0 B/op 0 allocs/op
|
可以看到将具体对象转换成 interface{}(以及反向操作)确实回带来一点点性能的影响,不过看起来影响倒不是很大。