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.
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:
Let’s take a closer look at how this works specifically in the Go programming language.
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:
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.
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
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 }
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.
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.
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.
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.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.