Reflection in Go: Everything you need to know to use it effectively
This post is written with the aim of helping Go developers understand and effectively use reflection in their projects
Hey, you’re in luck because today we’re discussing something that can really improve your Go code: the reflection
package.
“Hold on, doesn’t using reflection just make your code slower in exchange for making things easier?”
Well, if you’ve seen my earlier post on top methods to boost Go’s performance, you might think so.
But let’s not rush to judgment, when you use reflection in the correct way, and stick to best practices, it can turn out to be quite helpful.
Think of it as enjoying a really good slice of cake, you may feel a bit guilty eating it, but the experience makes it worthwhile.
1. Take a quick look
Reflection in Go gives you the power to inspect and modify variables and their types while the program is running. This makes it possible to find out a variable’s type, alter its value, or even invoke its methods.
In this section, we’re just skimming the surface of what reflection can do.
The reflect package offers two principal types,reflect.Type
and reflect.Value,t
hese types help you look into the type and value of any given variable.
reflect.Type
is used to describe the type of a value, and reflect.Value
provides details, about an instance of that value.
To get these details, you can use the functions reflect.TypeOf()
for type and reflect.ValueOf()
for value.
package main
import (
"fmt"
"reflect"
)
func main() {
num := 123
numType := reflect.TypeOf(num)
numValue := reflect.ValueOf(num)
fmt.Println("Type:", numType)
fmt.Println("Value:", numValue)
}
// Type: int
// Value: 123
reflect.Value
This type wraps around any given value and lets you extract info about it or carry out operations on it.
For example, you can use its Kind()
method to find out what kind of value ‘x’ holds (in this case, it’s an ‘int’). You can also use its Int()
method to get the actual integer value of ‘x
’, or the Set()
method to modify the value.
Here’s a code snippet to help you understand:
func main() {
x := 10
p := &x
v := reflect.ValueOf(p) // v is reflect.Value of pointer of x
fmt.Println("Kind of x:", v.Kind()) // "ptr"
v = v.Elem() // now v is reflect.Value of x (not pointer anymore)
fmt.Println("Kind of x:", v.Kind()) // "int"
fmt.Println("Value of x:", v.Int()) // 10
v.SetInt(20)
fmt.Println("Value of x after change:", x) // 20
}
Just to clarify: a reflect.Value
contains the value itself, not the variable. If the value happens to be a pointer to some struct, the reflect.Value
will capture that pointer, not the actual value it points to.
reflect.Type
The reflect.Type
in Go offers all you need to know about a type, including its name and structure, this way, it’s your reliable source for information about any given type.
You can use this to find details like the name of the type or even to figure out what kind of variable you’re dealing with, be it a struct, a slice, or a pointer:
type Car struct {
Model string
Year int
EngineSize float64
}
func main() {
var car Car
t := reflect.TypeOf(car)
fmt.Println("Name:", t.Name()) // "Car"
fmt.Println("Kind:", t.Kind()) // "struct"
fmt.Println("Number of fields:", t.NumField()) // 3
// iterate all fields of Car struct
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Println("Field name:", field.Name)
fmt.Println("Field type:", field.Type)
}
}
// Field name: Model
// Field type: string
// Field name: Year
// Field type: int
// Field name: EngineSize
// Field type: float64
2. Common Usages
Reflection in Go isn’t just for show; it’s a robust feature with a broad range of applications.
Here are some of them:
Custom struct tags (discussed in my article): With this method, you can attach your own metadata to the fields in a struct. This metadata is then accessible and operational when the program is running.
Dynamic type verification and assertions: Use reflection to confirm a variable’s type during runtime and employ type assertions to make sure the variable is exactly the type you think it is.
Iterating over struct fields: If you don’t know a struct’s type ahead of time, no worries. It allows you to go field by field through any struct when the code is running.
Implementing dependency injection: You can actually build your own dependency injection system using reflection with simple methods like ValueOf, Set,...
So, it’s not just one or two things, reflection is a tool you’ll find in many different packages to make all kinds of handy utilities.
3. Utilize on reflect package
In this section, we’ll take a thorough look at reflection in Go, covering key concepts and functionalities you’ll need for working with it effectively.
1. Using the reflect.Value
value.Interface() .Int() .String()…: These methods help you get the value from a
reflect.Value
object as an interface{}, int, or string.
But be careful, wrong usage, like calling .Int()
on a string, can trigger a panic.
value.Elem(): This method is your go-to for getting the real value inside a
reflect.Value
. For instance, if you have a pointer,Elem()
will dereference it for you.
func main() {
x := 10
xPtr := &x
val := reflect.ValueOf(xPtr)
val = val.Elem()
fmt.Println("Value:", val.Int())
}
// Value: 10
If you attempt reflect.ValueOf(x).Elem()
, expect a panic. The method doesn't work with non-pointer types, and you'll get an error message like "reflect: call of reflect.Value.Elem on int Value".
You might also want to consider using the Indirect()
function but I'll talk more about that later.
value.Kind(): This method lets you know the basic type, or ‘Kind’, of a
reflect.Value
.IsNil(): Checks if a value is nil and it will only work with certain types:
chan
,func
,interface
,map
,ptr
, orunsafePointer
. Using it on other types will trigger a panic.Set(x Value): You can set a new value for ‘v’ using ‘x’. But remember, if
CanSet()
returns false or the types aren't compatible, it's panic time again
2. Using the reflect.Type
Many of the functions in the reflect.Type are used for inspecting structs. You can use them to create dynamic structs while your code is running or even to personalize struct tags.
Name(): gives you the type’s name.
Kind(): tells you what the underlying type is.
Size(): Shows the type’s size in bytes.
If you’re dealing with fields of struct:
NumField(): Tells you how many fields a struct type has.
Field(i int) reflect.StructField: gets the field at position ‘i’ in a struct.
FieldByName(name string) (reflect.StructField, bool): Finds a field in the struct by its name and tells you if it exists or not.
Working with methods:
NumMethod(): counts the number of methods in a type.
Method(i int) reflect.Method: retrieves the i-th method in a type.
MethodByName(name string) (reflect.Method, bool): retrieves the method with the specified name in a type.
When working with non-primitive types:
Elem(): provides the type of the element for arrays, slices, pointers, or maps. An error will arise if the reflect.Type is not among these categories.
Len(): gives the length of an array type, but triggers an error if the type isn’t an array.
It’s important to note that using some of these methods in the wrong way (e.g. calling Elem()
on a non-pointer value) will cause panic.
3. Using reflect package
In earlier discussions, we’ve touched on the TypeOf(i any)
and ValueOf(i any)
functions from the reflect package:
reflect.Indirect(): consider this as a safer stand-in for “Elem()” when you’re not dealing with pointer types. If the input is a pointer, this function gives you the value it points to and if it’s not a pointer, you get the value itself.
func main() {
x := 10
val := reflect.ValueOf(x)
val = reflect.Indirect(val)
fmt.Println("Value:", val.Int())
}
// Value: 10
Zero(typ reflect.Type): this gives you a Value that holds the zero value for the type you specify.
Now, let’s face it.
Using reflection in Go does come with a performance hit. But many would argue that’s a reasonable trade-off for the extra power and flexibility you gain.
Take, for example, widely-used libraries like encoding/json — they employ reflection to simplify the developer experience.