You have no excuses now, use this free credit to launch your projects now on Digital Ocean, you're free to spend it whenever you want within the following 60 days.
Table of contents
Go with Reflect: Boost Your Code's Flexibility
Go with Reflect: Boost Your Code's Flexibility
The “reflect” package is a useful library in the Go programming language that provides functions for working with reflection.
What is reflection in programming?
Reflection is the ability of a program to inspect its own structure at runtime. This includes the ability to examine: types, values and metadata of objects in memory. Reflection allows us to have more flexible and generic code and allows us to create functions and data structures that can work with any type of object, regardless of its type. It is also the basis for metaprogramming.
We can divide Go’s reflection into two important types of data:
Type: The Go data type, from which all the information of the data type, its class, name, size, etc. is broken down. Value: The Go data value, with methods to modify the data of an object.
Data type: Type and TypeOf
Type is the most important data type in the reflect package since it represents the object in memory and stores all the meta information of our objects: its type, representation, methods and fields.
And how do we get a Type? With the TypeOf method
The TypeOf method of the reflect package is used to get the type of an interface, it receives an interface (i.e. anything) and returns a reflect.Type; “the most important type of reflect”.
In this diagram I place what I consider the most important methods of Type, but both reflect.Type and reflect.Value have many more.
See an example with TypeOf:
type Coffee struct {
Origin string `exampleTag:"value"`
Height int
}
coffeeInstance := Coffee{Origin: "Michoacan", Height: 1100}
// TypeOf returns the type *reflect.Type*
typeOfCoffee := reflect.TypeOf(coffeeInstance)
fmt.Println(typeOfCoffee)
// main.coffee
Notice how the data type is a structure corresponding to main, named coffee.
Name, the name of the data type
Name contains the name of the respective data type
// TypeOf returns a *reflect.Type* type
typeOfCoffee := reflect.TypeOf(coffeeInstance)
fmt.Println(typeOfCoffee.Name())
// coffee
Kind, the data type class
The TypeOf method allows us to obtain a Type object, which represents the data type of our interface, from which we can read the data type, with its Kind method.
// TypeOf returns a *reflect.Type* type
typeOfCoffee := reflect.TypeOf(coffeeInstance)
fmt.Println(typeOfCoffee.Kind())
// struct
NumField to get the number of fields in a struct
From Type we can obtain all kinds of useful information, such as the number of fields in an object. This is quite useful in the case of structs and other structures with multiple fields.
// We'll obtain the number of fields from our object
typeOfCoffee := reflect.TypeOf(coffeeInstance)
coffeeNumFields := typeOfCoffee.NumField()
fmt.Println(coffeeNumFields)
// 2
In this case we get 2 fields (Location and Height)
Consider that if you try to get the NumField of an int type or another type that does not have multiple fields, Go will return an error.
panic: reflect: Field of non-struct type int
Nested Field and Type of a struct
Type also puts at our disposal the Field method, which together with NumField, will allow us to access directly to the fields of a struct, from its index.
And, with this, we will be able to access the Types nested inside the Type object.
Each nested Type, like its parent, will have the Name and Kind methods that we saw a few lines above.
// We'll obtain the number of fields from our object
typeOfCoffee := reflect.TypeOf(coffeeInstance)
field := typeOfCoffee.Field(0)
fmt.Println(field.Type.Name())
fmt.Println(field.Type.Kind())
// string string
In this case, being primitive data types, both are the same: string; since the string type field is called string.
Read tags from a struct field with reflect
Reflect also allows us to access an object’s metadata, those annotations that you’ve probably seen if you’ve worked with ORM and/or JSON in Go.
To obtain the Tags of a struct, Tag.Get is used.
// We add a customized tag to read it in the future
type Coffee struct {
Origin string `exampleTag:"value"`
Height int
}
typeOfCoffee := reflect.TypeOf(coffeeInstance)
field := typeOfCoffee.Field(0)
// ...
fmt.Println(field.Tag.Get("exampleTag"))
// value
Data Value: Value and ValueOf
As I told you before, besides Type, there is another very important part in reflect, this is Value. The Value corresponds to the value of an interface, and how can we obtain a Value? Well, with ValueOf, do you notice how consistency is preserved?
The ValueOf method receives an interface as parameter and returns an object of type Value that will contain all the necessary methods to modify its own value.
Look at this example where you can see how ValueOf returns the value of our struct.
// ValueOf will return the value of our variable
coffeeInstance := Coffee{Origin: "Michoacan", Height: 1100}
ValueOfCoffee := reflect.ValueOf(coffeeInstance)
fmt.Println(ValueOfCoffee)
// {Michoacan 1100}
From Value we will have a series of methods to change the value of an object.
Modifying a field of a Value object
To modify a field we need to access the Value object that points to our object, which we can obtain by passing a pointer to the ValueOf method. And, from there, we will use Elem to access the value of our object, then its first field (only in the case of structs) and finally change the value using SetString.
coffeeInstance := Coffee{Origin: "Michoacan", Height: 1100}
valueofCoffee := reflect.ValueOf(&coffeeInstance)
valueofCoffee.Elem().Field(0).SetString("Oaxaca")
fmt.Println(coffeeInstance)
//{Oaxaca 1100}
Methods for assigning data in reflect
In addition to the SetString method, Go provides us with another set of specialized methods for each data type :
- SetBool(x bool)
- SetBytes(x []byte)
- SetCap(n int)
- SetComplex(x complex128)
- SetFloat(x float64)
- SetInt(x int64)
- SetIterKey(iter *MapIter)
- SetIterValue(iter *MapIter)
- SetLen(n int)
- SetMapIndex(key, elem Value)
- SetPointer(x puntero inseguro)
- SetString(x cadena)
- SetUint(x uint64)
- SetZero()
These methods can be accessed directly from Value.Elem for non-compound data types (such as structs).
original := "I will not be printed"
// We will obtain string's value
value := reflect.ValueOf(&original)
modified := "I WILL be printed"
// Let's change the original value
value.Elem().SetString(modified)
fmt.Printf(original)
// I WILL be printed
Modifying a field with the Set method
The specialized methods to “set” a data are very useful, but sometimes we need a more generic method that allows us to modify the values dynamically, and for that reflect provides us with Set, yes Set just like that, without anything else.
In this example, first we create a string, to later obtain its value, with ValueOf, and then pass this new value to the generic method Set, which receives a Value object. And where do we get Value from? Well, from passing any data to ValueOf.
someString := "Oaxaca"
newValue := reflect.ValueOf(someString)
valueofCoffee.Elem().Field(0).Set(newValue)
// {Oaxaca 1100}
Creating an object with reflect
Reflect also allows us to create objects dynamically from a Type. To do this, just pass as argument the type of object, Type, that we want to create, to the New method, which, as you know, can be easily obtained from TypeOf.
typeOfCoffee := reflect.TypeOf(coffeeInstance)
newInstance := reflect.New(typeOfCoffee)
fmt.Println(newInstance)
// &{ 0}
From the type we obtained with TypeOf reflect will create a new object. However, in this case, being a struct to which we have not specified the values of its fields, it will be initialized with its zero values .
And that’s it for the little introduction to reflect, now you know enough to get confused and cry while working with anything that needs reflection. Great isn’t it?