Analysis of golang reflect source code

abstract

By using the reflection function of Golang, this paper sets the member variable value of the structure, and understands the implementation principle of reflection by reading the source code in this process.

1, rtype structure

Empty interfaces are widely used in the reflect ion implementation of golang. Empty interfaces are represented by emptyInterface structure in the source code implementation. The code is as follows.

type emptyInterface struct {
	typ  *rtype         // Type information pointing to actual data
	word unsafe.Pointer // Points to the address of the data in memory
}

Each data type (such as int32,long,slice, pointer and user-defined data structure) in golang will contain an rtype structure. Rtype describes the general information of each type, and each specific type (such as pointer type and structure type) is the unique information of rtype + specific type. Take structures and pointer types as examples. Their data structures in the source code are as follows.

// Structure type
type structType struct {
	rtype                 // Type information of the structure itself
	pkgPath name
	fields  []structField // Information of each member variable of the structure
}

// Pointer type
type ptrType struct {
	rtype       // Pointer type
	elem *rtype // The type of data the pointer points to
}

In the implementation, the rtype is always used as the representation of the type, and the specific type is implemented through forced type conversion when necessary. For example, figure 1 is a piece of code in the reflect package, which forces the rtype to structType. From the above code, we can see the layout of pointer type and structure type in memory, as shown in Figure 2.

func (t *rtype) FieldByName(name string) (StructField, bool) {
	
	tt := (*structType)(unsafe.Pointer(t))
	
}

Figure 1

Figure 2

2, Examples

This example analyzes the execution process of the code and the data structure involved by setting the member variable value of the structure through reflection. In the example, a User structure is defined, the address is assigned to the empty interface, and the member variable value of the User structure is changed by reflection method.

package main
import (
	"fmt"
	"reflect"
)

type User struct {
	Name  string
	Phone string
	Age   int32
}

func main() {
	user := User{}
	var eface interface{} = &user      // Save the pointer and the data pointed to by the pointer in the null interface
	value := reflect.ValueOf(eface)    // Generate reflection object Value
	elem := value.Elem()               // Gets the Value of the object pointed to by the pointer
	field := elem.FieldByName("Phone") // Find Phone member variable
	field.SetString("0755-8000")       // Set the value of the Name variable to rtx
	fmt.Println(user.Name)             // Output "0755-8000"
}

The layout of the defined variable user in memory is shown in Figure 3. In the implementation of golang's string, each string occupies 16 bytes, x is the starting address of the user structure, the offset of Name is 0, the offset of Phone is 16, and the offset of Age is 32.

Figure 3

var eface interface{} = &user

After the above statement is compiled by the compiler, the data structure in Figure 4 will be generated. The type saved in the typ field of the empty interface eface represents the & user type (pointer type), and the word pointer points to the user's address in memory.

Figure 4

The Value structure in reflect represents the reflection object of go variable. The data structure of Value is shown in Figure 5.

type Value struct {
    typ *rtype          // Type information
    ptr unsafe.Pointer  // Can point to the address of the variable
    flag                // Meta information flag of variable
}

Figure 5

The ValueOf method of reflect converts the empty interface into a Value structure. ValueOf mainly calls the unpackEface function. The unpackEface function first converts the empty interface i into an emptyInterface address, and then obtains the typ field and word field in eface. These two fields are the type field and memory address of the go variable.

func ValueOf(i interface{}) Value {
	......
	return unpackEface(i)
}

// Convert null interface to Value
func unpackEface(i interface{}) Value {
	e := (*emptyInterface)(unsafe.Pointer(&i))  // Address cast
	t := e.typ                                  // Get type structure pointer
	......
	return Value{t, e.word, f}                  // Construct Value structure
}

Through the above analysis, after executing the following code, there will be a data structure as shown in Figure 6.

value := reflect.ValueOf(eface)

In the value structure, typ stores the & user type, that is, ptrType. ptrType mainly stores two information: the first is the type information of the pointer itself. The second is the elem member in ptrType, which represents the data type pointed to by the pointer (here is user type). User is the structure of user type, and golang uses structType data structure to represent the structure type.

Figure 6

At this time, typ in value is the pointer type, and ptr points to the user structure. * elem in the ptrType structure is the type pointed to by the pointer. To set the value of the member variable in the structure through reflection, you must find the user type. The elem method of value does this. The code of elem is shown in Figure 7. Elem generates a new value structure. Type points to structType and ptr still points to user structure.

func (v Value) Elem() Value {
	k := v.kind()         // value is a pointer type
	switch k {
	case Ptr:
		ptr := v.ptr  // Copy ptr pointer directly
		......
		tt := (*ptrType)(unsafe.Pointer(v.typ)) // rtype type cast to ptrType
		typ := tt.elem                          // Get the data type pointed to by the pointer. Here is the User structure type
		......
		return Value{typ, ptr, fl}              // Construct a new Value structure 
	}
	.....
}

Figure 7

Through the above code analysis, after executing the following code, a new Value instance elem will be generated. The relationship between data structures is shown in Figure 8.

elem := value.Elem()

Figure 8

The data structure of structType is shown in Figure 9. The User structure we defined has three members Name, Phone and Age. The compiler will generate the data structure shown in Figure 9. Each member variable is represented by a structField structure, and each member variable has its own type, represented by a typ member. In the User structure, Name and Phone are of type string, and Age is of type int32. offsetEmbed of structField represents the memory offset of the field in the structure, which can help locate the members of the structure.

Figure 9

Draw the data structures of Fig. 8 and Fig. 9 together, as shown in FIG. 10. The lowest bit of the offsetField variable of structField is used to indicate whether the field is an embedded field. In the figure, for convenience, the lowest bit is ignored and the offset is directly represented by 0,16,32.

Figure 10

To change the member variable value of the structure, you need to find the location of the structure member in memory, which can be found through the FieldByName method. This method first executes v.typ FieldByName (name), convert the rtype into structType, and then call the FieldByName method of structType. This method traverses the field slice of structType in Figure 10, finds the member variable name matching the input parameter name, and then returns the structfile structure pointer. The index in structfile indicates the position of the member variable in the structure. It can be considered that the index is the subscript in the file slice in structType, The structField structure can be found through this index.

func (v Value) FieldByName(name string) Value {
	....
	if f, ok := v.typ.FieldByName(name); ok { // Find the structField representing the member through the member variable name
		return v.FieldByIndex(f.Index)    // Find structField through index and return Value
	}
	return Value{}
}

func (t *rtype) FieldByName(name string) (StructField, bool) {
	
	tt := (*structType)(unsafe.Pointer(t))  // Cast type
	return tt.FieldByName(name)             // Traverse the field slice through the structure member variable name
}

// and a boolean to indicate if the field was found.
func (t *structType) FieldByName(name string) (f StructField, present bool) {
	....
	if name != "" {
	    for i := range t.fields {       // Traverse each member variable of the structure
	        tf := &t.fields[i]       
		if tf.name.name() == name { // Compare member variable names
		    return t.Field(i), true // Matching results returned
		}
		
	}
	
}

func (v Value) FieldByIndex(index []int) Value {
	if len(index) == 1 {
		return v.Field(index[0])
	}
	....
}
// Get the i th member of the structure
func (v Value) Field(i int) Value {
	
	tt := (*structType)(unsafe.Pointer(v.typ))
	
	field := &tt.fields[i]    // Information of the ith member
	typ := field.typ          // Type of the i th member
	
        // v.ptr points to the memory address of user, field The offset () method is the offset of the structure member variable in memory
        // v.ptr + offset is the address of the member variable
	ptr := add(v.ptr, field.offset(), "") 
        return Value{typ, ptr, fl}  // The returned Value represents a member variable of the structure
}

Through the above analysis, after executing the following statements, there will be a data structure as shown in Figure 11, and the newly generated Value is field.

field := elem.FieldByName("Phone")

Figure 11

The ptr pointer of field points to the user memory start address offset of 16, and then execute the following statement

field.SetString("0755-8000")

field's SetString method directly operates the memory address pointed to by ptr. The address at offset=16 is the address where the Phone member variable is located. This completes the setting of the Phone member variable of the User structure through reflection.

func (v Value) SetString(x string) {
	....
	*(*string)(v.ptr) = x
}

Tags: Go

Posted by nemethpeter on Fri, 15 Apr 2022 19:56:15 +0930