DEV Community

Cover image for Metaprogramming in Golang: Reflection Package guide
Geoffrey Sagini Mogambi
Geoffrey Sagini Mogambi

Posted on

Metaprogramming in Golang: Reflection Package guide

Reflection package is a very important feature in Golang because it allows metaprogramming, which is the capability of an application to examine its own structure. It works with types and it's used to analyze types at runtime. Types, Values and Kinds are the foundation of the reflect package. The reflection package allows you to extract the type and value from any interface{} variable.

Let's implement the reflection package with examples.

reflect.TypeOf()

The TypeOf function is used to describe the reflect.type value that is passed into the TypeOf method.

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var1 := "Reflection Package"
    fmt.Println(reflect.TypeOf(var1))

    var2 := 100
    fmt.Println(reflect.TypeOf(var2))

    var3 := true
    fmt.Println(reflect.TypeOf(var3))

    var4 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    fmt.Println(reflect.TypeOf(var4))

    var5 := map[string]interface{}{"Name": "Paul", "Number": 1000}
    fmt.Println(reflect.TypeOf(var5))
}

Enter fullscreen mode Exit fullscreen mode

output

string
int
bool
[]int
map[string]interface {}
Enter fullscreen mode Exit fullscreen mode

The output will show the various data types representation assigned by the variable. If the variable assigned is a string, the typeOf will be a string. If it is a Map variable the typeOf will show type Map and so on.

reflect.Value()

The reflect.value is used to show the underlying value of the respective variable passed to reflect.ValueOf method.

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var1 := "Reflection Package"
    fmt.Println(reflect.ValueOf(var1))

    var2 := 100
    fmt.Println(reflect.ValueOf(var2))

    var3 := true
    fmt.Println(reflect.ValueOf(var3))
    fmt.Println(reflect.ValueOf(&var3))

    var4 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    fmt.Println(reflect.ValueOf(var4))

    var5 := map[string]interface{}{"Name": "Paul", "Number": 1000}
    fmt.Println(reflect.ValueOf(var5))
}

Enter fullscreen mode Exit fullscreen mode

output

Reflection Package
100
true
0xc00001a0c0
[1 2 3 4 5 6 7 8 9 10]
map[Name:Paul Number:1000]
Enter fullscreen mode Exit fullscreen mode

reflect.Kind()

Kind is a property of reflect.Type whose results display basic types and generic complex types. When using kind, for built in types Kind and Type will be the same but for the custom types they will differ.

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var1 := "Reflection Package"
    fmt.Println(reflect.TypeOf(var1).Kind())

    var2 := 100
    fmt.Println(reflect.TypeOf(var2).Kind())

    var3 := true
    fmt.Println(reflect.TypeOf(var3).Kind())

    var4 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    fmt.Println(reflect.TypeOf(var4).Kind())

    var5 := map[string]interface{}{"Name": "Paul", "Number": 1000}
    fmt.Println(reflect.TypeOf(var5).Kind())
}

Enter fullscreen mode Exit fullscreen mode

output

string
int
bool
slice
map
Enter fullscreen mode Exit fullscreen mode

reflect.Copy()

The copy method will copy contents from the source to the destination until either the source has been exhausted or the destination has been filled. Both Source and Destination must have a kind of slice or array and the elements in the slice or array must be of the same type. If the source has elements of string type the equivalent type of string should also be used in the destination. The same scenario will be applied to other types like boolean, integers, floats, and any other type.

package main

import (
    "fmt"
    "reflect"
)

func main() {
    source := reflect.ValueOf([]int{1, 2, 3, 4})
    destination := reflect.ValueOf([]int{5, 6, 7, 8})

    // Copy() function retuns number of elements copied
    counter := reflect.Copy(destination, source)

    fmt.Println(counter)
    fmt.Println(source)
    fmt.Println(destination)
}

Enter fullscreen mode Exit fullscreen mode

output

4
[1 2 3 4]
[1 2 3 4]
Enter fullscreen mode Exit fullscreen mode

reflect.DeepEqual()

The DeepEqual method will return either True or False if the value in your case X and Y types follow the following rules.

  • Array values are deeply equal if their corresponding elements are deeply equal.
  • Struct values are deeply equal if their corresponding fields both exported and unexported are deeply equal.
  • Func values are deeply equal if both are nil otherwise they are not deeply equal.
  • Interface values will be said to be deeply equal if they hold equal concrete deep values.
  • For Map values deep equal applies when all of the following is true; they are both nil or both are non nil, they have the same length and either of them has the same Map object or corresponding Keys (matched using GO equality) map to deeply equal values.
  • Pointer values are deeply equal if using == operator shows equality or if they point to deeply equal values.
  • Slice values are deep equal when they have the same length and both are nil or non nil.
  • For other type values like strings, bools, numbers and channels they are deeply equal if using == operator shows equality.
package main

import (
    "fmt"
    "reflect"
)

type Employee struct {
    name    string
    address int
    email   string
}

func main() {
    // DeepEqual checks whether slices are are equal
    slice1 := []string{"a", "b", "c", "d"}
    slice2 := []string{"g", "h", "i", "j"}
    result := reflect.DeepEqual(slice1, slice2)
    fmt.Println(result)

    slice3 := []string{"a", "b", "c", "d"}
    slice4 := []string{"a", "b", "c", "d"}
    result2 := reflect.DeepEqual(slice3, slice4)
    fmt.Println(result2)

    // DeepEqual will check whether the arrays are equal
    arr1 := [3]int{10, 20, 30}
    arr2 := [3]int{10, 20, 30}
    result = reflect.DeepEqual(arr1, arr2)
    fmt.Println(result)

    // DeepEqual will check whether the structs are equal
    emp1 := Employee{"Sagini", 10259, "sagini@gmail"}
    emp2 := Employee{"Kamau", 6789, "kamau@gmail"}
    result = reflect.DeepEqual(emp1, emp2)
    fmt.Println(result)
}

Enter fullscreen mode Exit fullscreen mode

output

false
true
true
false
Enter fullscreen mode Exit fullscreen mode

reflect.Swapper()

The Swapper method is used to swap elements in a given slice and will throw a panic should the underlying interface{} provided not be a slice. Indexes of the respective values are used to do the swap.

package main

import (
    "fmt"
    "reflect"
)

func main() {
    swapSlice := []int{1, 2, 3, 4, 5, 6, 7}
    swap := reflect.Swapper(swapSlice)
    fmt.Printf("Original slice is :%v\n", swapSlice)

    // Swapper() function swaps elements in the provided slice using their index
    swap(1, 6)
    fmt.Printf("After the swap slice is :%v\n", swapSlice)
}
Enter fullscreen mode Exit fullscreen mode

output

Original slice is :[1 2 3 4 5 6 7]
After the swap slice is :[1 7 3 4 5 6 2]
Enter fullscreen mode Exit fullscreen mode

reflect.NumField()

The NumField method returns the number of fields in a given struct.

package main

import (
    "fmt"
    "reflect"
)

type Employee struct {
    Name    string
    Address int
    Email   string
}

func main() {
    emp := Employee{"Geoffrey", 675757, "geoff@yahoo"}
    empType := reflect.TypeOf(emp)
    fmt.Println(empType.NumField())
}
Enter fullscreen mode Exit fullscreen mode

output

3
Enter fullscreen mode Exit fullscreen mode

reflect.Field()

The Field method returns the reflect.value of the field that is name of the field accessed and it's type at the specified index.

package main

import (
    "fmt"
    "reflect"
)

type Employee struct {
    Name    string
    Address int
    Email   string
}

func main() {
    emp := Employee{"Geoffrey", 675757, "geoff@yahoo"}
    empType := reflect.TypeOf(emp)

    // create a loop to loop over the struct
    for i := 0; i < empType.NumField(); i++ {
        field := empType.Field(i)
        fmt.Println(field.Index, field.Name, field.Type)
    }

}

Enter fullscreen mode Exit fullscreen mode

output

[0] Name string
[1] Address int
[2] Email string
Enter fullscreen mode Exit fullscreen mode

reflect.FieldByName()

The FieldByName method is used to either get or set a struct field value by using name of a given field.

package main

import (
    "fmt"
    "reflect"
)

type Address struct {
    Latitude  int
    Longitude int
    Places    string
}

func main() {
    addr := Address{10, 30, "HeavensPark"}
    fmt.Println(reflect.ValueOf(&addr).Elem().FieldByName("Latitude"))
    fmt.Println(reflect.ValueOf(&addr).Elem().FieldByName("Longitude"))
    fmt.Println(reflect.ValueOf(&addr).Elem().FieldByName("Places"))

    reflect.ValueOf(&addr).Elem().FieldByName("Latitude").SetInt(20)
    reflect.ValueOf(&addr).Elem().FieldByName("Longitude").SetInt(40)
    reflect.ValueOf(&addr).Elem().FieldByName("Places").SetString("TreeTops")

    fmt.Println(reflect.ValueOf(&addr).Elem().FieldByName("Latitude"))
    fmt.Println(reflect.ValueOf(&addr).Elem().FieldByName("Longitude"))
    fmt.Println(reflect.ValueOf(&addr).Elem().FieldByName("Places"))
}

Enter fullscreen mode Exit fullscreen mode

output

10
30
HeavensPark
20
40
TreeTops
Enter fullscreen mode Exit fullscreen mode

reflect.FieldByIndex()

The FieldByIndex method will return a nested field corresponding to an index. A panic will occur if during the evaluation of struct, it finds a nil pointer or a field that is not a struct.

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    firstName string
    lastName  string
}

type Account struct {
    Person
    accountName string
}

func main() {
    acc := Account{
        Person:      Person{"Sagini", "Geoffrey"},
        accountName: "Stanchart",
    }
    t := reflect.TypeOf(acc)

    fmt.Printf("%#v\n", t.FieldByIndex([]int{0}))
    fmt.Printf("%#v\n", t.FieldByIndex([]int{0, 0}))
    fmt.Printf("%#v\n", t.FieldByIndex([]int{0, 1}))
    fmt.Printf("%#v\n", t.FieldByIndex([]int{1}))
}

Enter fullscreen mode Exit fullscreen mode

output

reflect.StructField{Name:"Person", PkgPath:"", Type:(*reflect.rtype)(0x490ba0), Tag:"", Offset:0x0, Index:[]int{0}, Anonymous:true}
reflect.StructField{Name:"firstName", PkgPath:"main", Type:(*reflect.rtype)(0x48b4c0), Tag:"", Offset:0x0, Index:[]int{0}, Anonymous:false}
reflect.StructField{Name:"lastName", PkgPath:"main", Type:(*reflect.rtype)(0x48b4c0), Tag:"", Offset:0x10, Index:[]int{1}, Anonymous:false}
reflect.StructField{Name:"accountName", PkgPath:"main", Type:(*reflect.rtype)(0x48b4c0), Tag:"", Offset:0x20, Index:[]int{1}, Anonymous:false}

Enter fullscreen mode Exit fullscreen mode

reflect.MakeSlice()

The MakeSlice method creates a new slice value. From the created slice you can find its type, length and capacity.

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var intSlice []int
    var intType reflect.Value = reflect.ValueOf(&intSlice)
    createdSlice := reflect.MakeSlice(reflect.Indirect(intType).Type(), 5, 8)

    fmt.Println("Kind of slice is:", createdSlice.Kind())
    fmt.Println("Length of slice is:", createdSlice.Len())
    fmt.Println("Capacity of slice is:", createdSlice.Cap())
}

Enter fullscreen mode Exit fullscreen mode

output

Kind of slice is: slice
Length of slice is: 5
Capacity of slice is: 8
Enter fullscreen mode Exit fullscreen mode

reflect.MakeMap()

The MakeMap method will create a new Map of a specified type.

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var newMap map[string]interface{}
    var mapType reflect.Value = reflect.ValueOf(&newMap)
    mapCreated := reflect.MakeMap(reflect.Indirect(mapType).Type())

    fmt.Println("Kind of map created is", mapCreated.Kind())
}

Enter fullscreen mode Exit fullscreen mode

output

Kind of map created is map
Enter fullscreen mode Exit fullscreen mode

reflect.MakeChan()

The MakeChan method will create a new channel with a specified type and buffer-size. Since the created channel has a buffer size it is of type buffer channel. Unbuffered channels don't have a buffer size specified.

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var newChan chan string
    var chanType reflect.Value = reflect.ValueOf(&newChan)
    chanCreated := reflect.MakeChan(reflect.Indirect(chanType).Type(), 100)

    fmt.Println("Kind of channel created is:", chanCreated.Kind())
    fmt.Println("Capacity of channel create is:", chanCreated.Cap())
}

Enter fullscreen mode Exit fullscreen mode

output

The kind of channel created is: chan
The capacity of channel created is: 100
Enter fullscreen mode Exit fullscreen mode

reflect.MakeFunc()

The MakeFunc method is used to create a new function value.
It gets the new function of the given Type that wraps the function fn.

package main

import (
    "fmt"
    "reflect"
)

type Addition func(int64, int64) int64

func main() {
    typ := reflect.TypeOf(Addition(nil))

    // verify if function. If not function return an error
    if typ.Kind() != reflect.Func {
        panic("A function is expected")
    }

    add := reflect.MakeFunc(typ, func(args []reflect.Value) (results []reflect.Value) {
        a := args[0].Int()
        b := args[1].Int()
        return []reflect.Value{reflect.ValueOf(a + b)}
    })

    fn, ok := add.Interface().(Addition)
    if !ok {
        return
    }
    fmt.Println(fn(10, 5))
}

Enter fullscreen mode Exit fullscreen mode

output

15
Enter fullscreen mode Exit fullscreen mode

Conclusion

You have gone through how to use the reflection package in Golang. This is just the surface of its usage as it can be used in many areas of Golang. The advantage of Reflection is that it allows code to be flexible and handles unknown data types by analyzing their memory representation. However, this is not cost-free as it introduces complexity in code and slows down performance. You can learn more on reflection guidance here.

Top comments (0)