Go reflect
package provides the ability to get the type and value of an object at runtime, which can help us to abstract and simplify the code, achieve dynamic data acquisition and method invocation, improve development efficiency and readability, and make up for Go
’s ability to handle data uniformly in the absence of generics.
With reflect
, we can achieve the ability to get object types, object fields, object methods, get tag
information of struct
, dynamically create objects, whether objects implement specific interfaces, convert objects, get and set object values, call Select
branches dynamically, etc. It looks good, but we all know one thing: there is a performance cost to using reflect!
Test
The use of reflect in Java also has an impact on performance, but unlike Java
reflect
, Java
does not distinguish between Type
and Value
types, so at least in Java we can pre-cache the corresponding reflect objects to reduce the impact of reflection on performance, but there is no way to pre-cache reflect in Go, because the Type type does not contain the runtime value of the object, you must go through ValueOf and runtime instance objects to get the Value object.
Reflection generation and acquisition of objects will add additional code instructions, and will also involve interface{}
boxing/unboxing operations, and may increase the generation of temporary objects in the middle, so performance degradation is certain, but how much can be reduced, or the data to speak.
Of course, the different methods used by reflect, and the different types of objects, will more or less affect the performance of the test data, we will take a common struct type as an example.
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
|
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
}
}
|
Test results:
1
2
3
4
5
6
|
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
|
Test results
We conducted tests for two functions.
- Creation of objects (struct)
- Assignment of object fields
For object creation, it takes 70 nanoseconds to generate the object through reflection, while it takes 45.6 nanoseconds to directly new the object, which is a big performance difference.
For the field assignment, there are four test cases.
Reflect_Set
: Generate an object through reflection, convert it to an actual object, and call the object’s fields directly for assignment, takes 73.6 nanoseconds
Reflect_SetFieldByName
: Generate the object by reflection and assign it by FieldByName, takes 492 nanoseconds
Reflect_SetFieldByIndex
: Generates an object by reflection and assigns it by Field, takes 111 nanoseconds
Direct_Set
: Assignment by calling the field of the object directly, in 43.1 nanoseconds
The main difference between the performance of Reflect_Set
and Direct_Set
is still in the generation of the object, because the assignment methods for the fields are the same afterwards, which is also consistent with the results of the test case
for object creation.
If the assignment is done by reflection, the performance degradation is very strong and the time consumption increases exponentially. Interestingly, the FieldByName
method is several times more powerful than the Field
method, because FieldByName
has an extra loop to find the field, although it still ends up calling Field
for the assignment.
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
|
unc (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
}
|
Optimization
From the results of the above tests, reflection can affect the performance of object generation and field assignment, but it does simplify the code and provide a unified code for business logic, such as the standard library json
coding and decoding, rpc
service registration and invocation, some ORM
frameworks such as gorm
, etc., are used to process data through reflection, which is to be able to handle common types.
1
2
3
4
5
6
7
8
9
10
11
|
// https://github.com/golang/go/blob/master/src/encoding/json/decode.go#L946
......
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())})
}
......
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// https://github.com/jinzhu/gorm/blob/master/scope.go#L495
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
}
}
}
|
In our pursuit of high performance scenarios, we may need to avoid reflection calls as much as possible, such as unmarshal
for json
data, easyjson
avoids using reflection by means of generators.
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()
}
}
|
Some other codec libraries also provide this method of avoiding the use of reflection to improve performance.
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
|
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
}
|
Test results:
1
2
|
BenchmarkDirectInvoke-4 300000000 5.60 ns/op 0 B/op 0 allocs/op
BenchmarkInterfaceInvoke-4 200000000 6.64 ns/op
|
You can see that converting concrete objects to interface{}
(and the reverse operation) does have a small performance impact, but it doesn’t seem to be a big one.
Reference https://colobu.com/2019/01/29/go-reflect-performance/