Reflection and Introspection in Go: How to Look Under the Hood of Your Code
Imagine you are a mechanic who can not just replace a part in an engine, but also look inside, understand how it works, and even change its behavior on the fly. This is exactly the superpower that reflection and introspection in Go give to a developer. Sounds like magic? In reality, it is a powerful tool that underlies many libraries, ORM systems, and frameworks.
In this article, we will not just cover the theory. We will write real code that shows how to use reflect to analyze structs, create universal functions, and even "peek" at types at runtime. Ready to level up your Go skills? Let's go!
What are Reflection and Introspection? The Difference Between the Concepts
Before diving into the code, let's clearly distinguish two similar but different concepts:
- Introspection — is the ability of a program to "look at itself": find out the type of a variable, the structure of an object, the list of methods or fields. This is passive analysis.
- Reflection — is the ability not only to look but also to change the behavior of a program during execution: dynamically call methods, create type instances, modify field values.
In Go, the reflect package is responsible for both of these capabilities. It gives the developer tools that break the language's static typing, but do so in a controlled and safe way (if you know what you're doing).
First Step: Basic Use of reflect.TypeOf and reflect.ValueOf
The simplest way to start is to get information about any variable. The two main "pillars" of reflection in Go are the functions reflect.TypeOf() and reflect.ValueOf().
Example 1: Finding Out the Type and Value
package main
import ( "fmt" "reflect")
func main() { name := "PythonLib" age := 42 price := 99.99
// Introspection: looking at types fmt.Println("Type of name:", reflect.TypeOf(name)) fmt.Println("Type of age:", reflect.TypeOf(age)) fmt.Println("Type of price:", reflect.TypeOf(price))
// Getting the value and its Kind v := reflect.ValueOf(price) fmt.Printf("Value: %v, Kind: %v", v, v.Kind())}Output:Type of name: stringType of age: intType of price: float64Value: 99.99, Kind: float64
Notice v.Kind(). In Go, there is a difference between type and kind. For example, if you declare type MyInt int, then TypeOf() will return main.MyInt, while Kind() will return int. This detail is critical when writing generic functions.
Working with Structs: Introspection of Fields and Tags
One of the most common tasks where reflection in Go is needed is struct analysis. For example, you are writing your own ORM or validator that needs to "understand" what fields a struct has and what their tags are.
Example 2: Breaking Down a Struct Piece by Piece
type User struct { ID int `json:"id" validate:"required"` Name string `json:"name" validate:"min=3,max=50"` Email string `json:"email" validate:"email"`}
func inspectStruct(s interface{}) { t := reflect.TypeOf(s) v := reflect.ValueOf(s)
// Make sure it's a struct if t.Kind() == reflect.Ptr { t = t.Elem() v = v.Elem() }
if t.Kind() != reflect.Struct { fmt.Println("Error: expected a struct") return }
fmt.Printf("Analyzing struct: %s", t.Name()) fmt.Println("Fields:") for i := 0; i < t.NumField(); i++ { field := t.Field(i) value := v.Field(i)
fmt.Printf(" %d. %s (%s) = %v", i+1, field.Name, field.Type, value.Interface())