https://mp.weixin.qq.com/s/hc1dHdQm6m4a7tLOWmsVEQ
Reflection is basically an essential module in all languages. Although the word “reflect” is deeply ingrained in people, it is more WHY. what exactly is reflect and what law is it based on?
In this article, we’ll take a look at what reflect is and how it’s implemented in the Go language as an example.
What is Reflect
In computer science, reflection is the ability of a computer program to access, detect, and modify its own state or behavior at runtime (runtime). Metaphorically speaking, reflection is the ability of a program to “observe” and modify its own behavior while it is running (from Wikipedia).
Simply put, the application can observe the value of a variable at runtime and can modify it.
一个例子
An example of the most common reflect standard library, as follows.
import (
"fmt"
"reflect"
)
func main() {
rv := []interface{}{"hi", 42, func() {}}
for _, v := range rv {
switch v := reflect.ValueOf(v); v.Kind() {
case reflect.String:
fmt.Println(v.String())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fmt.Println(v.Int())
default:
fmt.Printf("unhandled kind %s", v.Kind())
}
}
}
Output results:
hi
42
unhandled kind func
The main thing in the program is the declaration of rv
variables of type interface{}
, which contain 3 different types of values, namely string, number, and closure.
It is common to use interface{}
without knowing what the specific base type of the referent is, so we will use the interface{}
type to make a pseudo generic
.
This leads to a new problem: since the input is interface{}
, what about the output?
Go is a strongly typed language, the input is interface{}, and the output is definitely not running, so it is inevitable that the type determination is inevitable, and this is where reflection is used, that is, the reflect standard library. After reflection, the type assertion (type) is done again. This is one of the most common reflection scenarios we encounter when writing programs.
Go reflect
The core of the reflect standard library is the reflect. The methods used in reflection revolve around these two, and the main meaning of the methods is as follows.
- TypeOf: Used to extract the type information of the input value.
- ValueOf: Used to extract information about the value of a stored variable.
reflect.TypeOf
example:
func main() {
blog := Blog{"Gin"}
typeof := reflect.TypeOf(blog)
fmt.Println(typeof.String())
}
Output results:
main.Blog
TypeOf successfully resolves the blog variable to be of type main.Blog, i.e. even the package is known. This seems normal from a human recognition point of view, but not from a programmatic point of view. How does he know what “he” is under which package?
Let’s trace the source code together to see.
func TypeOf(i interface{}) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
At the source level, there are three main pieces of operations involved in the TypeOf method, which are as follows.
- Pointer method to obtain an arbitrary type of addressable pointer value.
- Use the emptyInterface type for forced interface type conversion.
- Call the toType method to convert to an externally usable Type type.
The most informative of these is the rtype type in the emptyInterface structure.
type rtype struct {
size uintptr
ptrdata uintptr
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
equal func(unsafe.Pointer, unsafe.Pointer) bool
gcdata *byte
str nameOff
ptrToThis typeOff
}
The most important type in use is the rtype type, which implements all the interface methods of the Type type, so it can be returned directly as a Type type.
The Type is essentially an interface implementation that contains all the methods necessary to obtain a type.
type Type interface {
// Applies to all types
// Returns the number of bytes occupied by the type after memory alignment
Align() int
// only works on strcut type
// Returns the number of bytes occupied by the type after memory alignment
FieldAlign() int
// Returns the i-th method in the method set of this type
Method(int) Method
// Get the methods in the corresponding method set by method name
MethodByName(string) (Method, bool)
// Returns the number of methods exported in the method set of this type.
NumMethod() int
// Returns the name of the type
Name() string
...
}
It is advisable to go through it roughly, understand clearly what methods are available, and then look towards it. The main idea is to build an index for your brain, so that you can quickly check it on pkg.go.dev.
reflect.ValueOf
example:
func main() {
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x))
}
Output results:
value: 3.4
ValueOf successfully gets the value of the variable x as 3.4, which is a match with reflect.
ValueOf, the core source code is as follows.
func ValueOf(i interface{}) Value {
if i == nil {
return Value{}
}
escapes(i)
return unpackEface(i)
}
func unpackEface(i interface{}) Value {
e := (*emptyInterface)(unsafe.Pointer(&i))
t := e.typ
if t == nil {
return Value{}
}
f := flag(t.Kind())
if ifaceIndir(t) {
f |= flagIndir
}
return Value{t, e.word, f}
}
At the source level, there are several operations involved in the ValueOf method.
- Call escapes to make the variable i escape to the heap.
- Forcing the variable i to the emptyInterface type.
- Assembles the required information (which contains the specific type of the value and a pointer to it) into the reflect.Value type and returns it.
When to type convert
At what point does Go perform the type conversion when it calls reflect to perform a series of reflection actions?
After all, we are passing in float64, and the function is of type inetrface as an argument.
View the compilation below:
$ go tool compile -S main.go
...
0x0058 00088 ($GOROOT/src/reflect/value.go:2817) LEAQ type.float64(SB), CX
0x005f 00095 ($GOROOT/src/reflect/value.go:2817) MOVQ CX, reflect.dummy+8(SB)
0x0066 00102 ($GOROOT/src/reflect/value.go:2817) PCDATA $0, $-2
0x0066 00102 ($GOROOT/src/reflect/value.go:2817) CMPL runtime.writeBarrier(SB), $0
0x006d 00109 ($GOROOT/src/reflect/value.go:2817) JNE 357
0x0073 00115 ($GOROOT/src/reflect/value.go:2817) MOVQ AX, reflect.dummy+16(SB)
0x007a 00122 ($GOROOT/src/reflect/value.go:2348) PCDATA $0, $-1
0x007a 00122 ($GOROOT/src/reflect/value.go:2348) MOVQ CX, reflect.i+64(SP)
0x007f 00127 ($GOROOT/src/reflect/value.go:2348) MOVQ AX, reflect.i+72(SP)
...
Reflection is basically an essential module in all languages. So what reflect really uses is the interface type.
reflect.Set
example:
func main() {
i := 2.33
v := reflect.ValueOf(&i)
v.Elem().SetFloat(6.66)
log.Println("value: ", i)
}
Output results:
value: 6.66
From the output, we can see that after calling the reflect.ValueOf method, we use the SetFloat method to change the value.
One of the core methods is the Setter-related method, and we can see together how it is implemented in the source code.
func (v Value) Set(x Value) {
v.mustBeAssignable()
x.mustBeExported() // do not let unexported x leak
var target unsafe.Pointer
if v.kind() == Interface {
target = v.ptr
}
x = x.assignTo("reflect.Set", v.typ, target)
if x.flag&flagIndir != 0 {
typedmemmove(v.typ, v.ptr, x.ptr)
} else {
*(*unsafe.Pointer)(v.ptr) = x.ptr
}
}
- Check if the reflection object and its fields can be set.
- Check if the reflection object and its fields are exported (made public).
- Call the assignTo method to create a new reflection object and overwrite the original reflection object.
- Modifies the value of the pointer of the current reflection object according to the value of the pointer returned by the assignTo method.
Simply put, check if it can be set, then create a new object, and finally modify it. It is a very standard assignment process.
The Three Laws of Reflection
Reflection in the Go language is ultimately the implementation of three laws.
- Reflection goes from interface value to reflection object.
- Reflection goes from reflection object to interface value.
- To modify a reflection object, the value must be settable.
We will introduce and explain these three core laws as a way to understand what the various methods in Go Reflection are based on.
The First Law
The first law of reflection is: “A reflection can get a reflected object from an interface value (interface)”.
example:
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}
Output results:
type: float64
Some of you may be confused, but the variable x that I passed into the code is of type float64, so it’s not a reflection object from the interface. The basic type of the variable we pass in the code is float64, but the input to the reflect.TypeOf method is interface{}, which is essentially a type conversion within the Go language. This will be further explained later.
The second law
The second law of reflection is: “you can get the interface value (interface) from the reflected object”. It is the opposite of the first law and can be complementary to each other.
Example Code:
func main() {
vo := reflect.ValueOf(3.4)
vf := vo.Interface().(float64)
log.Println("value:", vf)
}
Output results:
value: 3.4
As you can see in the sample code, the variable vo is already a reflection object, and then we can use the Interface method provided to get the interface value (interface) and finally force the conversion back to our original variable type.
The third law
The third law of reflection is: “To modify a reflected object, the value must be modifiable”. The third law does not seem to be directly related to the first and second, but it is essential, because in engineering practice, the purpose of reflection is to get the value and type, and the second is to be able to modify its value.
Otherwise, the reflection can only see, can not move, it will cause the reflection is very chicken. For example, the hot update of the configuration in the application will definitely involve the change of variables related to the configuration items, and most of them will use reflection to change the initial value.
Translated with www.DeepL.com/Translator (free version)
Example Code:
func main() {
i := 2.33
v := reflect.ValueOf(&i)
v.Elem().SetFloat(6.66)
log.Println("value: ", i)
}
Output results:
value: 6.66
Looking at the result alone, the value of variable i does change from 2.33 to 6.66, which seems perfect. But looking at the code alone, there seems to be something “wrong” with how setting a reflection value is so “problematic”: the
- Why must I pass in a pointer reference to variable i?
- Why does the variable v need to be Elem before it is set?
The rebellious Gophper says I don’t want to set it that way, but does it work?
Example Code:
func main() {
i := 2.33
reflect.ValueOf(i).SetFloat(6.66)
log.Println("value: ", i)
}
Error message:
panic: reflect: reflect.Value.SetFloat using unaddressable value
goroutine 1 [running]:
reflect.flag.mustBeAssignableSlow(0x8e)
/usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:259 +0x138
reflect.flag.mustBeAssignable(...)
/usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:246
reflect.Value.SetFloat(0x10b2980, 0xc00001a0b0, 0x8e, 0x401aa3d70a3d70a4)
/usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:1609 +0x37
main.main()
/Users/eddycjy/go-application/awesomeProject/main.go:10 +0xc5
According to the above prompt, the sample program does not work properly because it uses “use non-addressable values”. And this is a hard requirement that is guarded against by the reflect standard library itself. The reason for this is that Go’s function calls are passed as value copies, so if you pass a value without a pointer reference, you will not be able to change the source value of the reflected object. So the Go standard library makes logical judgments about this to avoid problems. So to change the source value of a reflection object, we must actively pass in a pointer reference to the corresponding variable, call the Elem method of the reflect standard library to get the source variable pointed to by the pointer, and finally call the Set method to set it.
Summary
In this article, we learn how Go reflection is used and what laws it is based on. In addition, if we pay a little attention, it is easy to see that Go reflection is based on interface, and furthermore, many of the runtime functions in Go language are based on interface. On the whole, Go reflection revolves around three things: Type, Value, and Interface, which complement each other, and reflection is essentially directly related to Interface.