Go tutorial on the use of reflection

  • 2020-06-23 00:34:23
  • OfStack

Reflection is one of the high-level topics of the Go language. I'll try to make it as simple as possible.

This tutorial is divided into the following sections.

What is reflection? Why do you need to check variables to determine their type? reflect package reflect. Type and reflect. Value reflect.Kind The NumField() and Field() methods The Int() and String() methods Complete program Should we use reflection?

Let's discuss each of these chapters one by one.

What is reflection?

Reflection is the ability of a program to examine variables and values at run time and figure out their types. You may not get it, but that's ok. By the end of this tutorial, you will have a clear understanding of reflection, so follow our tutorial.

Why do you need to check variables to determine their type?

The first question everyone faces when learning about reflection is: if every variable in a program is defined by us and we know the type at compile time, why do we need to check the variable at run time and figure out its type? Yes, most of the time, but not always.

Let me explain 1. Let's write a simple program.


package main

import (
 "fmt"
)

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

Run on playground

In the above program, the type of i is known at compile time, and then we print out i on the next line. There's nothing special here.

Now you know what happens when you need to get the variable type at run time. Suppose we were to write a simple function that takes a structure as an argument and USES it to create an SQL insert query.

Consider the following procedure:


package main

import (
 "fmt"
)

type order struct {
 ordId  int
 customerId int
}

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

Run on playground

In the above program, we need to write a function that takes the structure variable o as an argument and returns the SQL insert query below.


insert into order values(1234, 567)

This function is easy to write. Let's write this function now.


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() {
 o := order{
  ordId:  1234,
  customerId: 567,
 }
 fmt.Println(createQuery(o))
}

Run on playground

In line 12, the createQuery function creates an insert query with two fields from o (ordId and customerId). The program will output:


insert into order values(1234, 567)

Now let's upgrade the query builder. What if we wanted it to be generic, applicable to any structure type? Let's use the program to understand 1.


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 {
}

func main() {

}

Our goal is to complete the createQuery function (line 16 of the above program), which takes any structure as an argument and creates an insert query based on its fields.

For example, if we pass in the following structure:


o := order {
 ordId: 1234,
 customerId: 567
}

The createQuery function should return:


insert into order values (1234, 567)

Similarly, if we pass in:


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

This function returns:


insert into employee values("Naveen", 565, "Science Park Road, Singapore", 90000, "Singapore")

Since the createQuery function should apply to any structure, it accepts interface{} as a parameter. For simplicity, we only deal with structures that contain fields of type string and int, but can be extended to contain fields of any type.

The createQuery function should apply to all structures. Therefore, to write this function, you must check the type of structure parameter passed to you at runtime, find the structure field, and then create the query. That's where reflection comes in. In the next step of this tutorial, we will learn how to implement it using the reflect package.

reflect package

In the Go language, reflect implements runtime reflection. The reflect package helps identify the underlying specific type and value of the interface{} variable. This is exactly what we need. The createQuery function takes the interface{} argument and creates an SQL query based on its specific type and value. This is where the reflect package can help us.

Before writing our generic query builder, we first need to understand several types and methods in the reflect package. Let's go through each of them.

reflect. Type and reflect. Value

reflect. Type represents the specific type of interface{}, while reflect. Value represents its specific value. reflect.TypeOf () and reflect.ValueOf () can return reflect.Type and reflect.Value, respectively. These two types are the basis for creating the query builder. Let's now use a simple example to understand the two types.


package main

import (
 "fmt"
)

type order struct {
 ordId  int
 customerId int
}

func main() {
 o := order{
  ordId:  1234,
  customerId: 567,
 }
 fmt.Println(o)
}
0

Run on playground

In the above program, the createQuery function on line 13 takes interface{} as the argument. In line 14, reflect.TypeOf receives the parameter interface{} and returns reflect.Type, which contains the specific type of the interface{} parameter passed in. Similarly, on line 15, the reflect.ValueOf function takes the argument interface{} and returns reflect.Value, which contains the specific value of the passed interface{}.

The above program prints:

[

Type main.order
Value {456 56}

]

As you can see from the output, the program prints the specific type and value of the interface.

relfect.Kind

There is another important type in the reflect package: Kind.

In reflection packages, the types of Kind and Type may look similar, but the differences can be clearly seen in the following program.


package main

import (
 "fmt"
)

type order struct {
 ordId  int
 customerId int
}

func main() {
 o := order{
  ordId:  1234,
  customerId: 567,
 }
 fmt.Println(o)
}
1

Run on playground

The above program will output:

[

Type main.order
Kind struct

]

I think you know the difference. Type represents the actual type of interface{} (main.Order in this case), while Kind represents a specific category of the type (struct in this case).

NumField() and Field() methods

The NumField() method returns the number of fields in the structure, while the Field(i int) method returns reflect.Value for the field i.


package main

import (
 "fmt"
)

type order struct {
 ordId  int
 customerId int
}

func main() {
 o := order{
  ordId:  1234,
  customerId: 567,
 }
 fmt.Println(o)
}
2

Run on playground

In the above program, because the NumField method can only be used on structures, we first check that the class of q is struct in line 14. The rest of the program is easy to understand without explanation. The program will output:

[

Number of fields 2
Field:0 type:reflect.Value value:456
Field:1 type:reflect.Value value:56

]

Int() and String() methods

Int and String can help us to take out reflect.Value as int64 and string respectively.


package main

import (
 "fmt"
 "reflect"
)

func main() {
 a := 56
 x := reflect.ValueOf(a).Int()
 fmt.Printf("type:%T value:%v\n", x, x)
 b := "Naveen"
 y := reflect.ValueOf(b).String()
 fmt.Printf("type:%T value:%v\n", y, y)

}

Run on playground

In line 10 of the above program, we take out ES249en.Value and convert it to int64, while in line 13 we take out ES252en.Value and convert it to string. The program will output:

[

type:int64 value:56
type:string value:Naveen

]

Complete program

Now that we have enough knowledge to complete our query builder, let's implement it.


package main

import (
 "fmt"
)

type order struct {
 ordId  int
 customerId int
}

func main() {
 o := order{
  ordId:  1234,
  customerId: 567,
 }
 fmt.Println(o)
}
4

Run on playground

In line 22, we first check if the passed parameter is a structure. In line 23, we use the Name() method to get the name of the structure from reflect.Type of the structure. In the next row, we use t to create the query.

On line 28, the case statement checks whether the current field is ES281en.Int, and if so, we take the value of the field and convert it to int64 using the Int() method. The if else statement is used to handle boundary cases. Add a log to understand why you need it. In line 34, we use the same logic to get string.

Additional checks were made to prevent the program from crashing when the createQuery function passed in an unsupported type. The rest of the program's code is self-explanatory. I recommend that you add logs where appropriate and check the output to better understand the program.

The program will output:


package main

import (
 "fmt"
)

type order struct {
 ordId  int
 customerId int
}

func main() {
 o := order{
  ordId:  1234,
  customerId: 567,
 }
 fmt.Println(o)
}
5

As for adding field names to the output query, I'll leave that as an exercise for the reader. Try modifying the program to print out the query in the following format.


package main

import (
 "fmt"
)

type order struct {
 ordId  int
 customerId int
}

func main() {
 o := order{
  ordId:  1234,
  customerId: 567,
 }
 fmt.Println(o)
}
6

Should we use reflection?

Now that we've shown reflection in action, let's consider a very real problem. Should we use reflection? I would like to answer this question by quoting Rob Pike's maxim about using reflection.

Clarity is better than cleverness. And the reflex is not one eye clear.

Reflection is a very powerful and advanced concept in the Go language and should be used with caution. Writing clean and maintainable code using reflection is 10 points more difficult. You should avoid using it whenever possible and only use reflection if you must.


Related articles: