Shalitha Suranga Programmer | Author of Neutralino.js and Jerverless

Reflection in Go: Use cases and tutorial

6 min read 1769

Reflection in Go: Use cases and tutorial

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 follows a unique design pattern that other modern programming languages typically don’t: it doesn’t modify the language’s grammar. Rather, the Go language’s development team extends Go’s standard library instead.

Therefore, Go’s standard library has almost all of 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.

In this tutorial, we are going to learn about Go’s reflection API. We’ll also walk through metaprogramming and cover some example use cases that you may encounter.

What are metaprogramming and reflection?

Before diving into the tutorial, we need to understand metaprogramming and reflection. We can treat our source codes in two ways: as code, and as data.

If we treat the source code as code, it is possible to execute the source code on a CPU like we always do.

On the other hand, if we think of source code as data, we can inspect and update it like we do for normal program process data. For example, you can list all the properties of a struct without knowing all of its properties.

Metaprogramming refers to a programming technique that treats the program as data. The metaprogramming techniques can inspect and process other programs, or the program itself, even during its execution.

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.

Therefore, we can do things like:

We made a custom demo for .
No really. Click here to check it out.

  • Inspect the properties of a struct
  • Check whether a function exists in a struct instance
  • Check an atomic type of an unknown variable with reflection APIs

Let’s take a closer look at how this works specifically in the Go programming language.

Use cases for reflection in Go

The reflection concept typically exposes a native API to inspect or modify the current program. You may be thinking, you already know about your program’s source code, so why do you even need to inspect the code you wrote via reflection? But reflection has many helpful use cases, as shown below:

  • 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

The following tutorial section will cover all of the fundamentals you need to implement the above use cases. Also, I’ll show you how to build a simple shell program with reflection API.

Now that we’ve covered the the theoretical concept behind the reflection, let’s get started with practical examples.

The Go reflection package offers us runtime reflection, so these examples inspect or manipulate the program structure during the execution time. Since Go is a statically-typed compiled language, its reflection API is created based on two key components: reflection Type and Value.

Inspecting the types of variables

First, we can inspect the variable types to get started with the reflect package. Look at the following code that prints the types of several variables.

package main
import (
    "fmt"
    "reflect"
)
func main() {
    x := 10
    name := "Go Lang"
    type Book struct {
        name string
        author string
    }
    sampleBook := Book {"Reflection in Go", "John"}
    fmt.Println(reflect.TypeOf(x)) // int 
    fmt.Println(reflect.TypeOf(name)) // string
    fmt.Println(reflect.TypeOf(sampleBook)) // main.Book
}

The above code snippet prints data types of the variables by using the reflect.TypeOf function. The TypeOf function returns a reflection Type instance that provides functions to access more information about the current type. For example, we can use the Kind function to get the primitive type of a variable. Remember that the above snippet shows the main.Book custom Type for the sampleBook variable — not the primitive struct type.

Change the above code as follows to get the primitive types.

package main
import (
    "fmt"
    "reflect"
)
func main() {
    x := 10
    name := "Go Lang"
    type Book struct {
        name string
        author string
    }
    sampleBook := Book {"Reflection in Go", "John"}
    fmt.Println(reflect.TypeOf(x).Kind()) // int 
    fmt.Println(reflect.TypeOf(name).Kind()) // string
    fmt.Println(reflect.TypeOf(sampleBook).Kind()) // struct
}

The above code snippet outputs struct for the third print instruction because the reflection Type’s Kind function returns a reflection Kind that holds the primitive type information.

We can also use the reflection Type’s Size function to get the number of bytes needed to store the current type. Look at the following code snippet:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    x := 10
    name := "Go Lang"
    type Book struct {
        name string
        author string
    }
    sampleBook := Book {"Reflection in Go", "John"}
    fmt.Println(reflect.TypeOf(x).Size())
    fmt.Println(reflect.TypeOf(name).Size())
    fmt.Println(reflect.TypeOf(sampleBook).Size())
}

The above code snippet outputs the storage sizes of the variables in bytes. The output may vary according to your computer’s Instruction Set Architecture (ISA). For example, 64-bit computers/operating systems will show an output like below:

8 // size of int
16 // size of StringHeader
32 // size of struct

Inspecting the value of a variable

Earlier, we inspected data type information. It is also possible to extract the values of variables with the reflection package. The following code prints values of the variables with the reflect.ValueOf function:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    x := 10
    name := "Go Lang"
    type Book struct {
        name string
        author string
    }
    sampleBook := Book {"Reflection in Go", "John"}
    fmt.Println(reflect.TypeOf(x)) // 10
    fmt.Println(reflect.ValueOf(name)) // Go Lang
    fmt.Println(reflect.ValueOf(sampleBook)) // {Reflection in Go John}
}

The ValueOf function returns a reflection Value instance based on the provided variable. Similar to the reflection Type, reflection Value also holds more information about the variable’s value. For example, if we need to extract the second field’s value of the Book struct, we can use the reflection Value’s Field function, as shown below.

package main
import (
    "fmt"
    "reflect"
)
func main() {
    type Book struct {
        name string
        author string
    }
    sampleBook := Book {"Reflection in Go", "John"}
    fmt.Println(reflect.ValueOf(sampleBook).Field(1)) // John
}

Changing the Value of a variable

Earlier, we inspected the structure of the code with several functions in the reflect package. It is also possible to change the running code via Go’s reflect API. See how the following code snippet updates a string field in a struct.

package main
import (
    "fmt"
    "reflect"
)
func main() {
    type Book struct {
        Name string
        Author string
    }
    sampleBook := Book {"Reflection in Go", "John"}
    val := reflect.ValueOf(&sampleBook).Elem()
    val.Field(1).SetString("Smith")
    fmt.Println(sampleBook) // {Reflection in Go Smith}
}

Here, we use the SetString function to change the string data in the struct field. When we are changing values, we need to have addressable and accessible fields. Therefore, the Book struct uses title-cased fields to export them to the reflection API. Moreover, we have to provide a pointer of the struct instance to the ValueOf function to get the addressable reflection Value to the above val variable.

Inspecting the details of a struct

Let’s write a code snippet to inspect all fields of a struct. During the inspection, we can display the name and value of each struct field.

package main
import (
    "fmt"
    "reflect"
)
func main() {
    type Book struct {
        Name string
        Author string
        Year int
    }
    sampleBook := Book {"Reflection in Go", "John", 2021}
    val := reflect.ValueOf(sampleBook)

    for i := 0; i < val.NumField(); i++ {
          fieldName := val.Type().Field(i).Name
          fieldValue := val.Field(i).Interface()
          fmt.Println(fieldName, " -> ", fieldValue)
    }

}

The NumField function returns the number of fields of the given struct instance. The Field function returns a StructField instance that holds struct field details based on the provided index.

Also, the Interface function returns the stored value of the selected struct field. The for loop assembles all things together and shows a summary of the Book struct. The above code is indeed dynamic, meaning that it will work even if you add a new field for the Book struct.

Inspecting methods and calling them by their string names

Let’s assume that you are implementing a custom command engine for a shell program, and you need to run Go functions based on user-entered commands. If there are few mapping methods, you may implement a switch-case statement.

But, what if there are hundreds of mapping methods? Then, we can call Go functions dynamically by name. The following basic shell program uses reflection.

package main
import (
    "fmt"
    "reflect"
    "bufio"
    "os"
)
type NativeCommandEngine struct{}
func (nse NativeCommandEngine) Method1() {
    fmt.Println("INFO: Method1 executed!")
}
func (nse NativeCommandEngine) Method2() {
    fmt.Println("INFO: Method2 executed!")
}
func (nse NativeCommandEngine) callMethodByName(methodName string) {
    method := reflect.ValueOf(nse).MethodByName(methodName)
    if !method.IsValid() {
        fmt.Println("ERROR: \"" + methodName + "\" is not implemented")
        return
    }
    method.Call(nil)
}
func (nse NativeCommandEngine) ShowCommands() {
    val := reflect.TypeOf(nse)
    for i := 0; i < val.NumMethod(); i++ {
        fmt.Println(val.Method(i).Name)
    }
}
func main() {
    nse := NativeCommandEngine{}
    fmt.Println("A simple Shell v1.0.0")
    fmt.Println("Supported commands:")
    nse.ShowCommands()
    scanner := bufio.NewScanner(os.Stdin)
    fmt.Print("$ ")
    for scanner.Scan() {
        nse.callMethodByName(scanner.Text()) 
        fmt.Print("$ ")
    }
}

First, the above shell program shows all supported commands. Then, the user can enter commands as they wish. Each shell command has a mapped method, and if a particular method doesn’t exist, the shell will print an error message, as shown below.

The shell prints an error

If you need to add a new command, you only need to create a new exported method. After that, the shell program will support the new method automatically, thanks to the reflection API.

Conclusion

Not all programming languages out there expose APIs for reflection, but the popular programming languages like Java, C#, JavaScript, and Go all have reflection APIs.

Reflection is a powerful feature that has some drawbacks. Reflection lets developers solve some problems by writing less code. However, reflection often affects the readability of your code, and it may slow down your program sometimes. Therefore, don’t overuse reflection  —  make sure your reflection-based code is readable and optimized.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Shalitha Suranga Programmer | Author of Neutralino.js and Jerverless

Leave a Reply