Golang reflect package

Golang Reflect

Hello, Gophers! Today we’ll get into a more in-depth look into a package in Go called reflect. It is on the more intermediate to advanced implementation side. In this tutorial, we’ll see what it is and how it is used. For a deeper understanding, you can also check out Implementing reflection in Reflection and Type Switching in Go.

What is reflection?

Reflection is a program’s ability to analyze its variables and values and determine their type at runtime. While reflection can be used to print out the type and values of known/defined data types (like int, float, String, etc.), it is much more useful when dealing with arbitrary or abstract component structs. We’ll see this in our own implementation, so don’t worry.

Check out the Laws of Reflection by Rob Pike, one of the Go developers.

The empty interface

As we discussed very briefly in Reflection and Type Switching in Go, the empty interface is a special datatype in Go such that if we store any value, say x:= 6.626, then the type information of x is not lost even if it is called “interface” and not “int” or “float”. We can also create another variable using x, and then it will not be of type “interface” but rather “float64”:

func main() {
	var x interface{}
	x = 6.626
	fmt.Printf("x: type = %T, value = %v\n", x, x)
	goo := x
	fmt.Printf("goo: type = %T, value = %v\n", goo, goo)
        x = int(2020)
        fmt.Printf("x: type = %T, value = %v\n", x, x)
}

Therefore if we have a variable that is of type empty interface, then we can assign different values to this variable because it seems like it contains two properties/fields: Type and Value. So when we are re-assigning the value of x to an int, we are simply updating the value of these two fields.

Now in Reflection and Type Switching in Go, we considered two custom types – ID and Person, and asking for a variable that contains a specific type can be done by type assertion:

//x.(<type>) is the format for type assertion

However, the drawback to using type assertion is that we have to be certain if the variable is of that type. Otherwise, your program may crash. A workaround may be by asking for a bool value along with the assertion:

if v, ok := x.(ID); ok

Or we could do a type switch using switch and case.

Unfortunately it does NOT work for types that we don’t know. So it won’t work for custom datatypes and custom function outputs.

Reflect type/value

So, let’s start with the reflect package by importing it:

package main

import (
	"fmt"
	"reflect"
)

Then consider the Typeof and ValueOf methods:

func main() {
	var x interface{}
	x = 6.626

	t := reflect.TypeOf(x)
	v := reflect.ValueOf(x)

reflect.TypeOf(x) returns a value of type reflect.Type which is a struct that hides some details about how Go identifies types (data abstraction) and adding a few methods to it that we can operate upon.

Similarly, reflect.ValueOf(x) returns a value of type reflect.Value.

Essentially, these two functions look inside our empty interface and extract the type and value information that’s not really there as a field, but acts like one.

So now we can change the previous piece of code to this and it’ll work the same:

	fmt.Printf("x: type = %v, value = %v\n", t, v)
	goo := x
	fmt.Printf("goo: type = %T, value = %v\n", goo, goo)

So, we can look at one easy method that this reflection has – Kind() :

func main() {
	var x interface{}
	x = &struct{ name string }{}

	t0 := reflect.TypeOf(x)
	v0 := reflect.ValueOf(x)
	fmt.Printf("x: type = %v, value = %v\n", t0, v0)

	x = new(string)

	t1 := reflect.TypeOf(x)
	v1 := reflect.ValueOf(x)
	fmt.Printf("x: type = %v, value = %v\n", t1, v1)

	fmt.Printf("t0: type = %v, kind = %v\n", t0, t0.Kind())
	fmt.Printf("t1: type = %v, kind = %v\n", t1, t1.Kind())
}

Here, we define a pointer to a struct and a string for comparison. If we go run main.go this, we’ll get:

Reflect Kind Typeof Go
Reflect Kind Typeof() Go

So the Kind method is like a super type, i.e., no matter what the pointer points to, Kind will output ptr. What else has the same kind?

a0 := [5]int{}
a1 := [10]int{}
//are the same kind (arrays), but very different types

//Similarly
b0 := make([]int{})
b1 := make([]float{})
//are also the same kind (slices), but very different types

References

  • https://golang.org/pkg/reflect/
  • https://godoc.org/google.golang.org/grpc/reflection