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}
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}
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}
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}
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}
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 i
th 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}
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}
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}
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