Today I’m going to talk about reflection, a language feature that is not used much, but is used by many frameworks or base libraries. Reflection is not unique to Go, but is found in other programming languages. The goal of this article is to give you a brief overview of the application scenarios and usage of reflection.

One of the things we can touch on when we write code is the tag of a structure field, which I will cover in a later article.

I’m going to walk you through reflection with an example of making a generic SQL constructor with reflection. This is to see a foreign blogger to write an example, think the idea is very good, I have improved it, so that the implementation of the constructor more rich.

Reference since the idea of this article: golangbot.com/reflection/, content of this paper is not just for the simple translation of the original, specific see the content below ~!

The content of this article has been included in the Go Development Reference Book warehouse, which has collected more than 70 development practices.

What is reflection

Reflection is the ability of a program to examine its variables and values at run time and find their types. As general as it sounds, I’ll take you step by step through the examples in the article.

Why we need reflection

When studying reflection, the first question that comes to everyone’s mind is “Why should we check the type of a variable at run time, when variables in the program were already typed when they were defined?” That’s true, but not always, and you might look at that and think, dude, what are you talking about, em… So let’s write a simple program to explain it.

package main

import (  
    "fmt"
)

func main(a) {  
    i := 10
    fmt.Printf("%d %T", i, i)
}
Copy the code

In the above program, the type of variable I is known at compile time, and we print its value and type on the next line.

Now let’s understand the need to know the type of a variable at runtime. Suppose we want to write a simple function that takes a structure as an argument and uses it to create an SQL insert statement.

Consider the following program

package main

import (  
    "fmt"
)

type order struct {  
    ordId      int
    customerId int
}

func main(a) {  
    o := order{
        ordId:      1234,
        customerId: 567,
    }
    fmt.Println(o)
}
Copy the code

We need to write an SQL statement that takes the structure O defined above and returns something like INSERT INTO Order VALUES(1234, 567). This function definition is easy to write, such as the following.

package main

import (  
    "fmt"
)

type order struct {  
    ordId      int
    customerId int
}

func createQuery(o order) string {  
    i := fmt.Sprintf("INSERT INTO order VALUES(%d, %d)", o.ordId, o.customerId)
    return i
}

func main(a) {  
    o := order{
        ordId:      1234,
        customerId: 567,
    }
    fmt.Println(createQuery(o))
}
Copy the code

The createQuery in the above example creates the SQL using the ordId and customerId fields of parameter O.

Now let’s define our SQL creation function more abstractly. Again, let’s use the program spec as an example. Let’s say we want to generalize our SQL creation function to apply to any structure.

package main

type order struct {  
    ordId      int
    customerId int
}

type employee struct {  
    name string
    id int
    address string
    salary int
    country string
}

func createQuery(q interface{}) string{}Copy the code

Our goal now is to modify the createQuery function to accept any structure as an argument and create INSERT statements based on the structure field. For example, if the parameter passed to createQuery is no longer a structure of type Order but a structure of type Employee

 e := employee {
        name: "Naveen",
        id: 565,
        address: "Science Park Road, Singapore",
        salary: 90000,
        country: "Singapore",}Copy the code

The INSERT statement it should return should be

INSERT INTO employee (name, id, address, salary, country) 
VALUES("Naveen".565."Science Park Road, Singapore".90000."Singapore")  
Copy the code

Since createQuery is to work with any structure, it needs an argument of type interface{}. For simplicity’s sake, let’s assume that the createQuery function only handles structures containing fields of type String and int.

The only way to write this createQuery function is to check the type of the argument passed to it at run time, find its fields, and then create the SQL. This is where reflection comes in. In the next steps, we’ll learn how to do this using the Go language’s reflection package.

Go language reflector pack

Reflection at runtime is implemented in the Go language’s reflect package, which helps identify the underlying type and value of an interface{} variable. When our createQuery function receives an argument of type interface{}, it needs to create and return an INSERT statement based on the underlying type and value of the argument, which is where the reflection package comes in.

Before we start writing our generic SQL generator functions, we need to take a look at some of the types and methods we’ll use in the Reflect package, which we’ll take a look at one by one.

Reflect. The Type and reflect the Value

The underlying concrete Type of a reflected variable of Type interface{} is reflected by reflect.type and the underlying Value is reflected by reflect.value. The Reflect package contains two functions reflect.typeof () and reflect.valueof () that convert variables of Type interface{} to reflect.type and reflect.value, respectively. These two types are the basis for creating our SQL generator functions.

Let’s write a simple example to understand both types.

package main

import (  
    "fmt"
    "reflect"
)

type order struct {  
    ordId      int
    customerId int
}

func createQuery(q interface{}) {  
    t := reflect.TypeOf(q)
    v := reflect.ValueOf(q)
    fmt.Println("Type ", t)
    fmt.Println("Value ", v)


}
func main(a) {  
    o := order{
        ordId:      456,
        customerId: 56,
    }
    createQuery(o)

}
Copy the code

The above program will print:

Type  main.order  
Value  {456 56} 
Copy the code

The createQuery function receives an argument of type interface{} and passes the argument to the reflect.Typeof and reflect.Valueof calls. From the output, we can see that the program prints the underlying concrete type and value for the interface{} type argument.

The three Laws of Go language reflex

Here are the three laws of reflection:

  1. Reflection objects can be reflected from interface values.
  2. An interface value can be reflected from a reflection object.
  3. To modify a reflection object, its value must be settable.

The first rule of reflection is that we can convert interface type variables in Go to reflection objects. The reflect.typeof and Reflect.valueof mentioned above do this. The second refers to the fact that we can convert a reflection type variable back to the interface type, and the last relates to whether the reflection value can be changed. For a detailed explanation of the three laws, please Go to delevin’s article on the implementation principle of Go reflection, which begins with an illustration of the three laws.

Let’s move on to the reflection we need to complete our SQL generator.

reflect.Kind

There is another very important type in the Reflect package, reflect.kind.

The reflect.Kind and reflect.Type types may look similar, as well as naming. The Kind and Type types are interchanged in some English phrases, but there is a big difference in reflection, as can be clearly seen in the following program.

package main
import (  
    "fmt"
    "reflect"
)

type order struct {  
    ordId      int
    customerId int
}

func createQuery(q interface{}) {  
    t := reflect.TypeOf(q)
    k := t.Kind()
    fmt.Println("Type ", t)
    fmt.Println("Kind ", k)


}
func main(a) {  
    o := order{
        ordId:      456,
        customerId: 56,
    }
    createQuery(o)

}
Copy the code

The program above will print

Type  main.order  
Kind  struct  
Copy the code

The output makes the difference clear. Reflect. Type denotes the actual Type of the interface, i.e. in this case main.order and Kind denotes the Type of the interface, i.e. main.order is a “struct” Type, and the similar Type map[string]string’s Kind should be “map”.

Reflection method to get structure fields

We can get the type attribute of a field under a structure using a method of type reflect.StructField. Reflect. StructField can be obtained in either of the following ways provided by reflect.Type.

// Get the number of fields in a structure
NumField() int
// Get the type object of the field in the structure according to index
Field(i int) StructField
// Get the type object of the field in the structure based on the field name
FieldByName(name string) (StructField, bool)
Copy the code

Reflect. structField is a struct type, from which we can know the base type of the field, the Tag, whether it has been exported, etc.

type StructField struct {
	Name string
	Type      Type      // field type
	Tag       StructTag // field tag string. }Copy the code

In contrast to the method for retrieving Field information provided by reflect.Type, reflect.Value also provides a method for retrieving Field values.

func (v Value) Field(i int) Value {
...
}

func (v Value) FieldByName(name string) Value {
...
}
Copy the code

This one needs to be paid attention to, otherwise it’s confusing. Let’s try to get the field name and value of the order structure type by reflection

package main

import (
	"fmt"
	"reflect"
)

type order struct {
	ordId      int
	customerId int
}

func createQuery(q interface{}) {
	t := reflect.TypeOf(q)
	ift.Kind() ! = reflect.Struct {panic("unsupported argument type!")
	}
	v := reflect.ValueOf(q)
	for i:=0; i < t.NumField(); i++ {
		fmt.Println("FieldName:", t.Field(i).Name, "FiledType:", t.Field(i).Type,
			"FiledValue:", v.Field(i))
	}

}
func main(a) {
	o := order{
		ordId:      456,
		customerId: 56,
	}
	createQuery(o)

}
Copy the code

The above program will print:

FieldName: ordId FiledType: int FiledValue: 456
FieldName: customerId FiledType: int FiledValue: 56
Copy the code

In addition to getting the name and value of the structure field, you can also get the Tag of the structure field, which I’ll summarize later in the article before it gets too long.

Reflect. Value converts to the actual Value

We now need to complete our SQL generator by converting reflect.value to the Value of the actual type. Reflect.value implements a series of Int(), String(), Float() methods to convert it to the Value of the actual type.

Build an SQL generator with reflection

Now that we’ve covered all the prerequisites for writing this SQL generator function, let’s string them together to complete the createQuery function.

The complete implementation and test code for this SQL generator is as follows:

package main

import (
	"fmt"
	"reflect"
)

type order struct {
	ordId      int
	customerId int
}

type employee struct {
	name    string
	id      int
	address string
	salary  int
	country string
}

func createQuery(q interface{}) string {
	t := reflect.TypeOf(q)
	v := reflect.ValueOf(q)
	ifv.Kind() ! = reflect.Struct {panic("unsupported argument type!")
	}
	tableName := t.Name() // Extract the SQL table name from the structure type
	sql := fmt.Sprintf("INSERT INTO %s ", tableName)
	columns := "("
	values := "VALUES ("
	for i := 0; i < v.NumField(); i++ {
		// Note that reflects. Value also implements NumField,Kind methods
		Var ield(I).kind () = t.ield (I).type.kind ()
		switch v.Field(i).Kind() {
		case reflect.Int:
			if i == 0 {
				columns += fmt.Sprintf("%s", t.Field(i).Name)
				values += fmt.Sprintf("%d", v.Field(i).Int())
			} else {
				columns += fmt.Sprintf(", %s", t.Field(i).Name)
				values += fmt.Sprintf(", %d", v.Field(i).Int())
			}
		case reflect.String:
			if i == 0 {
				columns += fmt.Sprintf("%s", t.Field(i).Name)
				values += fmt.Sprintf("'%s'", v.Field(i).String())
			} else {
				columns += fmt.Sprintf(", %s", t.Field(i).Name)
				values += fmt.Sprintf(", '%s'", v.Field(i).String())
			}
		}
	}
	columns += "); "
	values += "); "
	sql += columns + values
	fmt.Println(sql)
	return sql
}

func main(a) {
	o := order{
		ordId:      456,
		customerId: 56,
	}
	createQuery(o)

	e := employee{
		name:    "Naveen",
		id:      565,
		address: "Coimbatore",
		salary:  90000,
		country: "India",
	}
	createQuery(e)
}
Copy the code

You can run the code locally. The above example will output the corresponding standard SQL insert statement based on the different structure arguments passed to the function

INSERT INTO order (ordId, customerId); VALUES (456.56); 
INSERT INTO employee (name, id, address, salary, country); VALUES ('Naveen'.565.'Coimbatore'.90000.'India'); 
Copy the code

conclusion

This article teaches you the basics of the Go language by using reflection in a practical application. While reflection seems powerful, it is difficult to write clean and maintainable code using reflection and should be avoided if possible and only used when absolutely necessary.

In my opinion, there is no need to use reflection to write business code, and libraries such as Encoding/JSON and GORM can take advantage of reflection to make coding easier for library users.

The content of this article has been included in the Go Development Reference Book warehouse, which has collected more than 70 development practices.