Reflection in Go

Welcome to tutorial no. 35 in Golang tutorial series.

Reflection is one of the advanced topics in Go. I will try to make it as simple as possible.

This tutorial has the following sections.

  • What is reflection?
  • What is the need to inspect a variable and find its type?
  • reflect package
    • reflect.Type and reflect.Value
    • reflect.Kind
    • NumField() and Field() methods
    • Int() and String() methods
  • Complete program
  • Should reflection be used?

Let’s discuss these sections one by one now.

What is reflection?

Reflection is the ability of a program to inspect its variables and values at run time and find their type. You might not understand what this means but that’s alright. You will get a clear understanding of reflection by the end of this tutorial, so stay with me.

What is the need to inspect a variable and find its type?

The first question anyone gets when learning about reflection is why do we even need to inspect a variable and find its type at runtime when each and every variable in our program is defined by us and we know its type at compile time itself. Well, this is true most of the time, but not always.

Let me explain what I mean. Let’s write a simple program.

 1package main
 2
 3import (
 4	"fmt"
 5)
 6
 7func main() {
 8	i := 10
 9	fmt.Printf("%d %T", i, i)
10}

Run in playground

In the program above, the type of i is known at compile time and we print it in the next line. Nothing magical here.

Now let’s understand the need to know the type of a variable at run time. Let’s say we want to write a simple function which will take a struct as an argument and will create a SQL insert query using it.

Consider the following program,

 1package main
 2
 3import (
 4	"fmt"
 5)
 6
 7type order struct {
 8	ordId      int
 9	customerId int
10}
11
12func main() {
13	o := order{
14		ordId:      1234,
15		customerId: 567,
16	}
17	fmt.Println(o)
18}

Run in playground

We need to write a function that will take the struct o in the program above as an argument and return the following SQL insert query,

insert into order values(1234, 567)

This function is simple to write. Let’s do that now.

 1package main
 2
 3import (
 4	"fmt"
 5)
 6
 7type order struct {
 8	ordId      int
 9	customerId int
10}
11
12func createQuery(o order) string {
13	i := fmt.Sprintf("insert into order values(%d, %d)", o.ordId, o.customerId)
14	return i
15}
16
17func main() {
18	o := order{
19		ordId:      1234,
20		customerId: 567,
21	}
22	fmt.Println(createQuery(o))
23}

Run in playground

The createQuery function in line no. 12 creates the insert query by using the ordId and customerId fields of o. This program will output,

insert into order values(1234, 567)

Now let’s take our query creator to the next level. What if we want to generalize our query creator and make it work on any struct. Let me explain what I mean by using a program.

 1package main
 2
 3type order struct {
 4	ordId      int
 5	customerId int
 6}
 7
 8type employee struct {
 9	name string
10	id int
11	address string
12	salary int
13	country string
14}
15
16func createQuery(q interface{}) string {
17}
18
19func main() {
20	
21}

Our objective is to finish the createQuery function in line no. 16 of the above program so that it takes any struct as argument and creates an insert query based on the struct fields.

For example, if we pass the struct below,

1o := order {
2	ordId: 1234,
3	customerId: 567
4}

Our createQuery function should return,

insert into order values (1234, 567)

Similarly if we pass

1 e := employee {
2        name: "Naveen",
3        id: 565,
4        address: "Science Park Road, Singapore",
5        salary: 90000,
6        country: "Singapore",
7    }

it should return,

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

Since the createQuery function should work with any struct, it takes a interface{} as argument. For simplicity, we will only deal with structs that contain fields of type string and int but this can be extended for any type.

The createQuery function should work on any struct. The only way to write this function is to examine the type of the struct argument passed to it at run time, find its fields and then create the query. This is where reflection is useful. In the next steps of the tutorial, we will learn how we can achieve this using the reflect package.

reflect package

The reflect package implements run-time reflection in Go. The reflect package helps to identify the underlying concrete type and the value of a interface{} variable. This is exactly what we need. The createQuery function takes a interface{} argument and the query needs to be created based on the concrete type and value of the interface{} argument. This is exactly what the reflect package helps in doing.

There are a few types and methods in the reflect package which we need to know first before writing our generic query generator program. Lets look at them one by one.

reflect.Type and reflect.Value

The concrete type of interface{} is represented by reflect.Type and the underlying value is represented by reflect.Value. There are two functions reflect.TypeOf() and reflect.ValueOf() which return the reflect.Type and reflect.Value respectively. These two types are the base to create our query generator. Let’s write a simple example to understand these two types.

 1package main
 2
 3import (
 4	"fmt"
 5	"reflect"
 6)
 7
 8type order struct {
 9	ordId      int
10	customerId int
11}
12
13func createQuery(q interface{}) {
14	t := reflect.TypeOf(q)
15	v := reflect.ValueOf(q)
16	fmt.Println("Type ", t)
17	fmt.Println("Value ", v)
18		
19	
20}
21func main() {
22	o := order{
23		ordId:      456,
24		customerId: 56,
25	}
26	createQuery(o)
27
28}

Run in playground

In the program above, the createQuery function in line no. 13 takes a interface{} as argument. The function reflect.TypeOf in line no. 14 takes a interface{} as argument and returns the reflect.Type containing the concrete type of the interface{} argument passed. Similarly the reflect.ValueOf function in line no. 15 takes a interface{} as argument and returns the reflect.Value which contains the underlying value of the interface{} argument passed.

The above program prints,

Type  main.order
Value  {456 56}

From the output, we can see that the program prints the concrete type and the value of the interface.

reflect.Kind

There is one more important type in the reflection package called Kind.

The types Kind and Type in the reflection package might seem similar but they have a difference which will be clear from the program below.

 1package main
 2
 3import (
 4	"fmt"
 5	"reflect"
 6)
 7
 8type order struct {
 9	ordId      int
10	customerId int
11}
12
13func createQuery(q interface{}) {
14	t := reflect.TypeOf(q)
15	k := t.Kind()
16	fmt.Println("Type ", t)
17	fmt.Println("Kind ", k)
18		
19	
20}
21func main() {
22	o := order{
23		ordId:      456,
24		customerId: 56,
25	}
26	createQuery(o)
27
28}

Run in playground

The program above outputs,

Type  main.order
Kind  struct

I think you will now be clear about the differences between the two. Type represents the actual type of the interface{}, in this case main.Order and Kind represents the specific kind of the type. In this case, it’s a struct.

NumField() and Field() methods

The NumField() method returns the number of fields in a struct and the Field(i int) method returns the reflect.Value of the ith field.

 1package main
 2
 3import (
 4	"fmt"
 5	"reflect"
 6)
 7
 8type order struct {
 9	ordId      int
10	customerId int
11}
12
13func createQuery(q interface{}) {
14	if reflect.ValueOf(q).Kind() == reflect.Struct {
15		v := reflect.ValueOf(q)
16		fmt.Println("Number of fields", v.NumField())
17		for i := 0; i < v.NumField(); i++ {
18			fmt.Printf("Field:%d type:%T value:%v\n", i, v.Field(i), v.Field(i))
19		}
20	}
21
22}
23func main() {
24	o := order{
25		ordId:      456,
26		customerId: 56,
27	}
28	createQuery(o)
29}

Run in playground

In the program above, in line no. 14 we first check whether the Kind of q is a struct because the NumField method works only on struct. The rest of the program is self explanatory. This program outputs,

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

Int() and String() methods

The methods Int and String help extract the reflect.Value as an int64 and string respectively.

 1package main
 2
 3import (
 4	"fmt"
 5	"reflect"
 6)
 7
 8func main() {
 9	a := 56
10	x := reflect.ValueOf(a).Int()
11	fmt.Printf("type:%T value:%v\n", x, x)
12	b := "Naveen"
13	y := reflect.ValueOf(b).String()
14	fmt.Printf("type:%T value:%v\n", y, y)
15
16}

Run in playground

In the program above, in line no. 10, we extract the reflect.Value as an int64 and in line no. 13, we extract it as string. This program prints,

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

Complete Program

Now that we have enough knowledge to finish our query generator, let’s go ahead and do it.

 1package main
 2
 3import (
 4	"fmt"
 5	"reflect"
 6)
 7
 8type order struct {
 9	ordId      int
10	customerId int
11}
12
13type employee struct {
14	name    string
15	id      int
16	address string
17	salary  int
18	country string
19}
20
21func createQuery(q interface{}) {
22	if reflect.ValueOf(q).Kind() == reflect.Struct {
23		t := reflect.TypeOf(q).Name()
24		query := fmt.Sprintf("insert into %s values(", t)
25		v := reflect.ValueOf(q)
26		for i := 0; i < v.NumField(); i++ {
27			switch v.Field(i).Kind() {
28			case reflect.Int:
29				if i == 0 {
30					query = fmt.Sprintf("%s%d", query, v.Field(i).Int())
31				} else {
32					query = fmt.Sprintf("%s, %d", query, v.Field(i).Int())
33				}
34			case reflect.String:
35				if i == 0 {
36					query = fmt.Sprintf("%s\"%s\"", query, v.Field(i).String())
37				} else {
38					query = fmt.Sprintf("%s, \"%s\"", query, v.Field(i).String())
39				}
40			default:
41				fmt.Println("Unsupported type")
42				return
43			}
44		}
45		query = fmt.Sprintf("%s)", query)
46		fmt.Println(query)
47		return
48
49	}
50	fmt.Println("unsupported type")
51}
52
53func main() {
54	o := order{
55		ordId:      456,
56		customerId: 56,
57	}
58	createQuery(o)
59
60	e := employee{
61		name:    "Naveen",
62		id:      565,
63		address: "Coimbatore",
64		salary:  90000,
65		country: "India",
66	}
67	createQuery(e)
68	i := 90
69	createQuery(i)
70
71}

Run in playground

In line no. 22, we first check whether the passed argument is a struct. In line no. 23 we get the name of the struct from its reflect.Type using the Name() method. In the next line, we use t and start creating the query.

The case statement in line. 28 checks whether the current field is reflect.Int, if that’s the case we extract the value of that field as int64 using the Int() method. The if else statement is used to handle edge cases. Please add logs to understand why it is needed. Similar logic is used to extract the string in line no. 34.

We have also added checks to prevent the program from crashing when unsupported types are passed to the createQuery function. The rest of the program is self explanatory. I recommend adding logs at appropriate places and checking their output to understand this program better.

This program prints,

insert into order values(456, 56)
insert into employee values("Naveen", 565, "Coimbatore", 90000, "India")
unsupported type

I would leave it as an exercise for the reader to add the field names to the output query. Please try changing the program to print query of the format,

insert into order(ordId, customerId) values(456, 56)

Should reflection be used?

Having shown a practical use of reflection, now comes the real question. Should you be using reflection? I would like to quote Rob Pike’s proverb on the use of reflection which answers this question.

Clear is better than clever. Reflection is never clear.

Reflection is a very powerful and advanced concept in Go and it should be used with care. It is very difficult to write clear and maintainable code using reflection. It should be avoided wherever possible and should be used only when absolutely necessary.

This brings us to and end of this tutorial. Hope you enjoyed it. Please leave your feedback and comments. Please consider sharing this tutorial on twitter and LinkedIn. Have a good day.

Next tutorial - Reading Files