Golang -- deep and simple out, which takes you to use the reflection of golang to build a public background query method

Some basic methods

This article will not introduce the basic concepts and principles of reflection. It will start with each common method and explain some basic and advanced usage. Reflection is not suitable for use in the business layer, because it will reduce the running speed geometrically, and the robustness of the program made with reflection is not high. Once a link is not handled properly, it will directly affect the running of the program, but in the future It is suitable for use on the stage, which can greatly reduce the code amount, and free from the complicated operation of adding, deleting, changing and checking and endless err (error oriented programming, too appropriate).

reflect.TypeOf()

You can get the type object of any variable. With this object, you can get the Name and Kind of the variable. The Name represents the Name of the variable type, and the Kind represents the Name of the underlying type of the variable. Here are two typical examples.

// System variables
str := "Zhang San"
reflectType := reflect.TypeOf(str)
fmt.Printf("name: %v kind: %v", reflectType.Name(), reflectType.Kind())	// name: string kind: string

// Custom variable
type person string
a := person("Zhang San")
reflectType := reflect.TypeOf(a)
fmt.Printf("name: %v kind: %v", reflectType.Name(), reflectType.Kind())	// name: person kind: string

Elem() method

It is mainly used to get the type object of pointer type (only used on array, chan, map, pointer, slice types)

str := "Zhang San"
reflectType := reflect.TypeOf(&str)
reflectElem := reflectType.Elem()
fmt.Printf("name: %v kind: %v", reflectElem.Name(), reflectElem.Kind())	// name: string kind: string

reflect.ValueOf()

You can get the value object of any variable. Its type is reflect.Value. You can also get the Name and Kind of the variable by using this object. You can get the value of the variable by using type assertion by getting Kind.

Here, reflect.ValueOf is not very effective. In practical application scenarios, first use reflect.ValueOf to get the reflection.value of the variable and then connect with Interface() method to convert the variable into Interface {} type. The methods to get the reflection.value mostly use reflect.TypeOf() and reflect.New() method. Later, the practical part will have detailed usage.

isNil()

To determine whether the value object is nil, it can only be used for channels, slices, arrays, map s, functions, interface s, etc.

isValid()

Judge whether the value object is a valid value, that is, it is not its default 0 value, for example, 0 of the number type and "" of the string type. In actual use, if these values are not processed, it may directly panic.

reflect.SliceOf()

Returns a slice type of a single type with reflect.TypeOf.

str := "Zhang San"
reflectType := reflect.TypeOf(str)
reflectSlice := reflect.SliceOf(reflectType)
fmt.Printf("name: %v kind: %v", reflectSlice.Name(), reflectSlice.Kind())	// name:  kind: slice

// Get the value of the element in the slice
a := []int{8, 9, 10}
reflectType := reflect.ValueOf(a)
for i := 0; i < reflectType.Len(); i++ {
  fmt.Println(reflectType.Index(i))
}
// 8  9   10

Note that there is no type name for array, pointer, slice, map and other types.

reflect.New()

Instantiate a value object of this type with reflect.TypeOf and return a pointer to the value object (you must use a pointer if you want to set a value with reflection).

str := "Zhang San"
reflectType := reflect.TypeOf(str)
reflectValue := reflect.New(reflectType)
// Set value	
reflectValue.Elem().SetString("Li Si")
fmt.Printf("value: %v kind: %v", reflectValue.Elem(), reflectValue.Elem().Kind())	// value: Li sikind: String

reflect.PtrTo()

Returns a pointer to the value object.

str := "Zhang San"
reflectType := reflect.TypeOf(str)
if reflectType.Kind() != reflect.Ptr {
  reflectType = reflect.PtrTo(reflectType)
}
fmt.Printf("value: %v kind: %v", reflectType, reflectType.Kind())	// value: *string kind: ptr

Reflection of structure

The above methods are just appetizers, but the reflection of structure is still commonly used. All kinds of addition, deletion, modification and query operations in business should be completed through database, while the interaction of database uses structure. Here, we will first list some methods to be used for reflection of structure, and then complete the content of this article through a background public model class practice.

The content related to the above basic methods will not be described here. If you are interested in it, you can try it in private. Here is only for some special methods of the structure.

Several methods of structure field correlation

NumField()

Returns the number of fields in the structure. NumField() must use a structure, otherwise it will panic.

type Student struct {
  Name string
  Age  int
}

a := &Student{
  Name: "Zhang San",
  Age:  18,
}
reflectValue := reflect.ValueOf(a)
fmt.Println(reflectValue.Elem().NumField())	// 2

Field()

The value of the field is obtained through the index of the field. Starting from 0, the order refers to the top-down order when the structure is defined.

a := &Student{
  Name: "Zhang San",
  Age:  18,
}
reflectValue := reflect.ValueOf(a)
for i := 0; i < reflectValue.Elem().NumField(); i++ {
  fmt.Println(reflectValue.Elem().Field(i))
}

FieldByName()

Gets the value of a field by its name.

a := &Student{
  Name: "Zhang San",
  Age:  18,
}
reflectValue := reflect.ValueOf(a)
fmt.Println(reflectValue.Elem().FieldByName("Name"))	// Zhang San

NumMethod()

Returns the number of methods for the structure.

FieldByNameFunc()

Returns the method with the corresponding name based on the anonymous function passed in.

Method()

Directly through the index of the method, return the corresponding method.

MethodByName()

Return the corresponding method by method name.

The functions related to the above four methods do not need to be exemplified. After obtaining the methods through the corresponding functions, Call() is used for calling. In particular, the parameters passed in when calling must be in the format of [] reflect.Value.

Practice part 1: write a common background query method

The database classes used here are gorm This article does not discuss its related knowledge. If you have any doubts, please practice on your own.

First write the model, create the folder model under the root directory, and create search.go in the model folder

// Student student
type Student struct {
	Name string
	Age  int
  ID int
}

// TableName table name
func (Student) TableName() string {
	return "student"
}

Write an interface to implement common methods, and create search.go in the root directory

// SearchModel search interface
type SearchModel interface {
	TableName() string
}

// SearchModelHandler stores some necessary information during the query
type SearchModelHandler struct {
	Model SearchModel
}

// GetSearchModelHandler get handler
func GetSearchModelHandler(model SearchModel) *SearchModelHandler {
	return &SearchModelHandler{
		Model: model,
	}
}

// Search Search
func (s *SearchModelHandler) Search() string {
	query := db.model(s.Model)
	itemPtrType := reflect.TypeOf(s.Model)
	if itemPtrType.Kind() != reflect.Ptr {
		itemPtrType = reflect.PtrTo(itemPtrType)
	}
	itemSlice := reflect.SliceOf(itemPtrType)
	res := reflect.New(itemSlice)

	// This step is very important. Although the Scan method receives an interface {} type, it will report an error if we directly use s.Model to execute the incoming SearchModel
	// The reason is that the interface of scan here is different from that of the model we passed in. Scan only knows the interface types defined in gorm package
	err := query.Scan(res.Interface()).Error
	if err != nil {
		// Don't follow me here
		panic("error")
	}

	ret, _ := json.Marshal(res)
	return string(ret)
}

Such a simple public class is born, and then it is called to create main.go under the directory change

func main() {
	handler := GetSearchModelHandler(&model.Student{})
	handler.Search()
}

Advanced level of actual combat: add additional information to a single table

For example, we also have a class table. When we return the student information, we need to add the class information. How can we do this? Here I only provide my own way of thinking. If there are better suggestions, please write them in the comments below for mutual exchange.

First, create the structure of class, and create class.go in the model folder

// Class class
type Class struct {
	ID   int
	Name string
}

// TableName table name
func (Class) TableName() string {
	return "class"
}

Then write a public interface and create the file additional_api.go in the model folder

// AdditionalInfo additional information for help
type AdditionalInfo struct {
	FieldName string
	Method    func(ids []int32) string
}

// The minmap API obtains the total content interface, which is equivalent to the SearchModel in practice I
type MinMapAPI interface {
	TableName() string
}

// MinMapInterface minimum information acquisition interface
type MinMapInterface interface {
	TransFields() string
}

The above method is defined first, and then useful. Then modify the content of the model, open class.go, and input

// ClassMin minimum class information
type ClassMin struct {
	ID   int
	Name string
}

// TransFields transform the name, fill in the name of the field you want to get
func (c *ClassMin) TransFields() string {
	return "Name"
}

Next, write a specific method to obtain additional information, open additional_api.go, and enter the following

// GetMinMap get minimum information
func GetMinMap(ids []int32, model MinMapAPI, minModel MinMapInterface) string {
	// Get slices of total data
	modelType := reflect.TypeOf(model)
	modelSliceType := reflect.SliceOf(modelType)
	res := reflect.New(modelSliceType)

	err := db.Model(model).Where("id in (?)", ids).Scan(res.Interface()).Error
	if err != nil {
		panic("error")
	}

	minModelType := reflect.TypeOf(minModel).Elem()
	resValue := res.Elem()
	resLen := resValue.Len()

	ret := make(map[int]MinMapInterface, resLen)
	for i := 0; i < resLen; i++ {
    // Get data of current subscript
		item := resValue.Index(i).Elem()
    // Get the field to get
		name := item.FieldByName(minModel.TransFields())
		id := item.FieldByName("ID")

    // Splicing return value
		setItem := reflect.New(minModelType)
		setItem.Elem().FieldByName("ID").SetInt(int64(id.Interface().(int)))
		setItem.Elem().FieldByName(minModel.TransFields()).SetString(name.Interface().(string))
    // The queried content is the specific model, where the type assertion is converted back to
		ret[id.Interface().(int)] = setItem.Interface().(MinMapInterface)
	}

	data, _ := json.Marshal(ret)
	return string(data)
}

Modify student.go and get additional data. An anonymous function is used here to ensure that each model has its own parameters and the reusability of the code

// AdditionalParams additional data parameters
func (s *Student) AdditionalParams() map[string]AdditionalInfo {
	return map[string]AdditionalInfo{
		"class": {
			FieldName: "ClassID",
			Method: func(ids []int32) string {
				return GetMinMap(ids, &Class{}, &ClassMin{})
			},
		},
	}
}

Correspondingly, we also need to modify search.go to add AdditionalParams method for excuses. Here, we directly paste the final code of search.go for comparison

// SearchModel search interface
type SearchModel interface {
	TableName() string
	AdditionalParams() map[string]model.AdditionalInfo
}

// SearchModelHandler stores some necessary information during the query
type SearchModelHandler struct {
	Model          SearchModel
	ListValue      reflect.Value
	AdditionalData string
}

// GetSearchModelHandler get handler
func GetSearchModelHandler(model SearchModel) *SearchModelHandler {
	return &SearchModelHandler{
		Model: model,
	}
}

// Search Search
func (s *SearchModelHandler) Search() interface{} {
	query := db.model(s.Model)
	itemPtrType := reflect.TypeOf(s.Model)
	if itemPtrType.Kind() != reflect.Ptr {
		itemPtrType = reflect.PtrTo(itemPtrType)
	}
	itemSlice := reflect.SliceOf(itemPtrType)
	res := reflect.New(itemSlice)

	// This step is very important. Although the Scan method receives an interface {} type, it will report an error if we directly use s.Model to execute the incoming SearchModel
	// The reason is that the interface of scan here is different from that of the model we passed in. Scan only knows the interface types defined in gorm package
	err := query.Scan(res.Interface()).Error
	if err != nil {
		// Don't follow me here
		panic("error")
	}
	s.ListValue = res.Elem()
	
	data, _ := json.Marshal(res)
	
	ret := map[string]string {
		"list": string(data),
		"additional": s.AdditionalData,
	}
	
	return ret
}

// GetAdditionalData get additional information
func (s *SearchModelHandler) GetAdditionalData() {
	additionParams := s.Model.AdditionalParams()
	list := s.ListValue
	listLen := list.Len()
	if len(additionParams) < 1 || list.Len() < 1 {
		s.AdditionalData = ""
		return
	}

	additionalIDs := make(map[string][]int)
	additionalData := make(map[string]string, len(additionParams))
	for i := 0; i < listLen; i++ {
		for key, val := range additionParams {
			fieldName := val.FieldName
			// Determine whether the key in the Map already exists
			if _, ok := additionalIDs[key]; !ok {
				additionalIDs[key] = make([]int, 0, listLen)
			}

			fields := list.Index(i).Elem().FieldByName(fieldName)

			if !fields.IsValid() {
				continue
			}

			additionalIDs[key] = append(additionalIDs[key], fields.Interface().(int))
		}
	}

	for k, v := range additionalIDs {
		additionalData[k] = additionParams[k].Method(v)
	}

	ret, _ := json.Marshal(additionalData)
	s.AdditionalData = string(ret)
}

Tags: Go JSON Database Programming

Posted on Mon, 13 Apr 2020 04:36:05 -0700 by Tagette