Go reflections deep dive— from structs and interfaces

Snir David
Apr 18, 2020 · 6 min read

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.

Image for post
Image for post
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?

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

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

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?

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?)

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

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

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

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

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

Complex types investigation

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!

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

The Startup

Medium's largest active publication, followed by +771K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store