Go reflections deep dive— from structs and interfaces

Snir David
The Startup
Published in
6 min readApr 18, 2020

--

Go reflections is a topic that requires understanding of the go internals regarding structs, interfaces and the type system in order to understand how it fundamentally works behind the scenes.

You can use reflections without going into these details, of course. This article objective is to introduce you to some of the details in a way that will enable deeper understanding. But those are not strictly required.

This article assumes you have basic understanding of structs and interfaces.
You may have a quick refresh in “Go by example” for struct and interfaces, or go deeper into the Go tour for structs and interfaces.

Reflection. Photo by Dawid Zawiła on Unsplash

What is reflection?

In computer science, reflection is the ability of a process to examine, introspect, and modify its own structure and behavior. — Wikipedia

Reflections are runtime manipulations of the program. It is a form of meta-programming, but not everything considered meta-programming is reflection.

Why does it matter for Go?

There are many aspects in which reflections matter. Through this guide I will focus on the most obvious one:
Go is a statically typed language, you have to declare all your types ahead of time. As such, you have no way of handling types you do not know ahead of time, even when the manipulation, examination or introspect you need to do with them does not require this knowledge ahead of time.

A good example is the print method of fmt . If you want to print the type of a variable using %T , the fmt package does not need to know about your own created struct for example called Person . But it still can print Person out.

The empty interface

An interface is a type with set of methods, which every struct that implements these methods are automatically implementing the interface. This allows to use the interface as a type for methods, that you can pass in the structs that implements it in.

For an empty interface then, every struct, and every “primitive” type inherently implements it.

Therefore, using the empty interface as type allows to pass in any type in its place. Like using any type, but not really.

The above code will work, and will print 101 for the first print, as it use + on x which is typed int, and then passes to myPrint , it sends it to Println that also accepts empty interface values, and using reflections internally.

But Go is still a statically typed language, so using the empty interface will not allow you to do anything with the variable (unless you use type assertions or reflections).

The above code will not compile. within myPrint the type of item is empty interface, so even though the underlying type is integer, go does not “know” about it yet, and therefore will panic.

Type assertions

Type assertions are a utility that help you validate the underlying type for some variable, and in case it is the type you asserted for, get the underlying value variable recognized as this type.

This will print out the number 10, as we first assert the type using myVar.(int) . Into v is assigned the typed variable if the type assertion is successful, and into ok is assigned a boolean value of whether the assertion was successful.

There is more to type assertions, if you are interested I recommend the Go tour for type assertions and type switches.

Where is the type details?

How does the type assertion (and by proxy, reflection too) do know of the underlying type of a more generic interface (empty interface or otherwise).

To understand that, we will take a look at the go implementation directly, at the time of writing in go/src/sync/atomic/value.go . That implements the basic value of every variable in go.

// ifaceWords is interface{} internal representation.                       type ifaceWords struct {
typ unsafe.Pointer
data unsafe.Pointer
}

Every empty interface, and by extension every value in go, has in its basic representation 2 unsafe pointers: typ and data .

typ holds the type information for the current variable, so even though a variable might be an empty interface, the actual type information is intact within typ .

data holds the value itself, alongside more data such as kind that is out of scope for this article. The important point is that data holds the value *and* some more things, whereas typ only holds the type data.

What is missing in type assertions? (Or: why do we need reflections?)

Type assertions allows for validation and usage of the underlying type for interfaces when you know what type to check for.

In the example above, we use assert for int specifically. That means we knew ahead of time the type, for the type assertion to work. Even if we switch and check for multiple options, we still have to know of the concrete type we assert in compile time.

In cases where we don’t know of the concrete types in compile time, reflections come in. Or in other words, as stated in the beginning of this article, when we need to do runtime examinations.

Think back to the fmt.Print example, the fmt package does not know about our structs types but can still print its value and type (using %T ).

Finally: Reflections

Reflect.Type and Reflect.Value

These are the two basic and most important types provided by the Reflect package. Reflect.Type and Reflect.Value are structs defined within the Reflect package only, and the reflect package have methods operating on interface variables that internally populate these structs using the underlying typ and data available to every interface in go.

Reflect.TypeOf() and Reflect.ValueOf() are the 2 basic methods available, respectively returning Reflect.Type and Reflect.Value , as shown in this example:

It’s worth taking a look at the same example with our own created structs too, for more clarity:

Using TypeOf and ValueOf gives back to us the underlying type, and a pointer to the value. It’s essential to understand the value coming from ValueOf is of type Reflect.Value, and not the original variable type.

So for example, in the int example above, we can’t take the value and do arithmetic, e.g myValue + 1 with it. It will fail to compile, as Go compiler does not recognize this action for Reflect.Value , but just for Integer types.

We’ll get into how you can use the value again, if needed.

Reflect.Kind

Understanding kinds might be tricky, and resources you might find online can be confusing as most of them handle kind system from type theory and talks about implementation in languages like Haskell.

What you need to know in regards to Go, is that every variable has a kind, derived from its type. Kinds are like the types of types.

It’s easiest to demonstrate with places where you create your own types: Structs.

Let’s go back to the code above where we create the Person struct. The person we creates has a type Person . The type of the Person type, namely, its Kind , is Struct .

Getting a type kind is done by applying the Kind() method on a Reflect.Type variable.

You can see the full list of kinds available here: https://golang.org/pkg/reflect/#Kind

For some instances, other than the obvious example of structs, kinds looks like duplication — e.g type int64 has kind of int64. Don’t stress it, for our purpose here knowing the kind is helpful with reutilizing the empty interface value (as Reflect.Value is not usable by itself, demonstrated in the section above).

Converting Reflect.Value back to original type value

So we have a value with type Reflect.Value after we analyzed an empty interface variable passed to us using Reflect.ValueOf() . But this value is not really usable, as Go type system does not recognize it as its original type, yet.

We want to convert the value itself to the original type. The process for that will be:

  1. Figure out the exact original type. Use Reflect.TypeOf() or Reflect.Kind() as needed.
  2. Get the raw value data using pointer to the value. (Unsafe pointer that is, but this is out of scope for now)
  3. Type cast the pointer.

Lucky for us, the reflect package already handles that for us for all basic types.

The basic types conversion methods

reflect package provides method on reflect.Value that convert the value back to a typed original value. Lets see it in action with an integer:

There are conversion methods available for all basic types, Bool , Float , String etc’ etc’.

Complex types investigation

The above covers basic types, but what do we do with Structs for example?

The reflect package provides methods to investigate structs “from the outside” as well. Receiving how many fields there are and receiving individual fields.

A code example will make it clear:

Thank you!

Thank you for putting the time into reading this article, I sincerely hope it was useful (:

You can follow me on twitter, I write about all different technologies: https://twitter.com/snird

--

--