Introduction
The Go programming language is a popular statically-typed, compiled programming language that has a C-like syntax. It is gaining more popularity every day in modern developer communities because of features such as memory safety, garbage collection, concurrency, performance, and a developer-friendly minimal syntax.
Go provides a set of libraries known as Go’s standard library
, these libraries has almost all the features we need for a modern programming language. It also offers a package to work with reflection
, which is a concept that comes from the metaprogramming
paradigm.
Metaprogramming
Metaprogramming is a programming technique in which computer programs have the ability to treat other programs as their data. It means that a program can be designed to read, generate, analyze or transform other programs, and even modify itself during runtime. It allows programs a greater flexibility to efficiently handle new situations without recompilation. Metaprogramming can be used to move computations from run-time to compile-time, to generate code using compile time computations, and to enable self-modifying code. The ability of a programming language to be its own metalanguage is called reflection. Reflection is a valuable language feature to facilitate metaprogramming.
Reflection
Reflection is a sub-topic of the metaprogramming paradigm. Almost all popular languages expose internal APIs to handle metaprogramming for the particular programming language itself. These APIs are known as reflection APIs, and they serve as a particular programming language’s ability to inspect, manipulate, and execute the structure of the code. Reflection provides us features like:
- Inspect the properties of a struct
- Check whether a function exists in a struct instance
- Check an atomic type of unknown variable with reflection APIs
The Go reflect
package gives you features to inspect and manipulate an object at runtime. Reflection is an extremely powerful tool for developers and extends the horizon of any programming language. Types, Kinds and Values are three important pieces of reflection that are used in order to find out information.
Reflection in Go
The foundation of Go reflection is based around Values, Types and Kinds.These are defined in the package and are of the type reflect.Value, reflect.Type and reflect.Kind and can be obtained using the methods:
- reflect.ValueOf(x interface{}): Function in Golang is used to get the new Value initialized to the concrete value stored in the interface x
- reflect.TypeOf(x interface{}) : Function in Golang is used to get the reflection Type that represents the dynamic type of i.
- Type.Kind(). : Function in Golang is used to find the name of kind.
Although, terms kind and types seems to be similar, A Type is the representation of a type in Go. For example, let say a user is a custom-defined type in go, the name assigned by the user is stored in as Type
, whereas A Kind is the representation of the type of Type
. For example in user custom-defined types, the data-type of the Type
will be the Kind
.
Let take a look on the code below:
package main
import (
"fmt"
"reflect"
)
type myType string
func main() {
i := myType("Hello World")
value := reflect.ValueOf(i)
kind := value.Kind()
typ := reflect.TypeOf(i)
fmt.Println("Value:- ", value, " Type:- ", typ, " Kind:- ", kind)
}
We have defined a custom type named myType
which takes the string value. The output of the above code will be:
Success #stdin #stdout 0s 5592KB
Value:- Hello World Type:- main.myType Kind:- string
As you can see the type of the aforementioned struct is main.myType
which would be its name and the kind is string
.
It is to be noted that, there are 3 ways to find the type of variables in Golang as follows:
- Using reflect.TypeOf Function
- Using reflect.ValueOf.Kind() Function
- Using %T with Printf
Use case and Implementation of reflection in Go
In modern metaprogramming paradigm, reflection can have many helpful use cases, some of them are:
- Programmers can use reflection to solve programming problems with less code
- e.g., if you are using a struct instance to build a SQL query, you can use reflection to extract struct fields without hardcoding every struct field name
- Since reflection offers a way to examine the program structure, it is possible to build static code analyzers by using it
- We can dynamically execute code with the help of the reflection API
- e.g., you can find existing methods of a struct and call them by name
Let's take use case to get a deeper understanding of reflection in go
.
Let's take a structure:
type DataReflect struct {
S string
I int
F float32
Mp map[string]interface{}
}
The above structure can store multiple data-types and also store nested structures. The use case is we need to process all the string that is stored within the above structure without actually reading the data.
Below is the main()
function for the above use case:
func main() {
data := &DataReflect{
S: "Hello",
I: 32,
F: 32.567,
Mp: map[string]interface{}{
"tag1": "World",
"tag2": 2,
"tag3": true,
"tag4": DataReflect{
S: "Reflection",
I: 45,
F: 89.234,
Mp: map[string]interface{}{},
},
},
}
reflectString(data)
}
In the above code, we have an object data which has a complex structure, if we have to implement the code for the aforementioned use case the code will be way too complex. Hence, reflection is much faster and easier way to deal with such use cases.
Below is the recursive function we have used:
func reflectString(data interface{}) {
rv := reflect.ValueOf(data)
rt := rv.Type().Kind()
if rt != reflect.Interface && rt != reflect.Map && rt != reflect.String && rt != reflect.Struct && rt != reflect.Ptr {
return
}
if rt == reflect.Map || rt == reflect.Ptr {
rv = reflect.Indirect(rv) // Indirect() used to get the value that rv points to
rt = rv.Type().Kind()
}
if rt == reflect.String {
fmt.Println(rv)
return
}
if rt == reflect.Struct {
for i := 0; i < rv.NumField(); i++ { //NumFields() returns number of fields in the structure
fv := rv.Field(i)
ft := fv.Type().Kind()
switch ft {
case reflect.String:
fmt.Println(fv)
case reflect.Map:
processMap(fv.Interface())
case reflect.Struct:
reflectString(fv.Interface())
}
}
}
}
func processMap(data interface{}) {
fv := reflect.ValueOf(data)
if fv.Kind() != reflect.Map {
return
}
for _, e := range fv.MapKeys() { // MapKeys() used to get a slice containing all the keys present in the map, in unspecified order.
t := fv.MapIndex(e).Elem().Type().Kind() //Elem used to get the value that the interface contains or that the pointer points to
v := fv.MapIndex(e).Elem()
switch t {
case reflect.String:
fmt.Println(v)
default:
reflectString(v.Interface())
}
}
}
The Output of the above code is:
/tmp/GoLand/___go_build_reflect_main_go
Hello
World
Reflection
Process finished with the exit code 0
It's worth to be noted, the NumField()
and .Field()
are only applicable to structs. A panic will be caused if the element is not a struct. Also while dealing with reflect.Ptr we should be mindful of using reflect.Indirect() from which we ca retrieve the value the pointer is pointing to.
Hence, we have to be very careful while implementing program using reflect package as it can easily trigger a panic
.
Conclusion
In this blog, we have explored the concept of metaprogramming and reflection in programming languages. Also, we have discussed various use cases and implementation of reflection to get the better understanding of it. Knowing the types of variables during run-time enables us to write a more flexible and faster code. Hence, reflection is one of the most importance feature of metaprogramming paradigm.