Introspection in Go - Unveiling Type and Value with Reflection
Daniel Hayes
Full-Stack Engineer · Leapcell

Introspection in Go: Unveiling Type and Value with Reflection
Go, renowned for its simplicity, performance, and strong type system, also offers a powerful mechanism for runtime introspection: reflection. While often considered an advanced topic, understanding Go's reflect
package is crucial for tasks like serialization, deserialization, ORMs, and building generic libraries. This article will explore how to use Go's reflection capabilities to obtain both type and value information from variables dynamically.
What is Reflection?
At its core, reflection is the ability of a program to examine and modify its own structure and behavior at runtime. In Go, this means being able to inspect the type of a variable, access and modify its underlying value, and even call methods dynamically, all without knowing the concrete type at compile time.
Go's reflection is provided by the reflect
package. The fundamental types you'll interact with are reflect.Type
and reflect.Value
.
The reflect.Type
Interface: Understanding Type Information
reflect.Type
represents the static type of a variable. It provides methods to query information about the type itself, such as its name, kind, underlying type, and whether it's a pointer, struct, slice, etc.
To obtain a reflect.Type
from an interface, you use the reflect.TypeOf
function:
package main import ( "fmt" "reflect" ) func main() { var i int = 42 var s string = "hello Go" var b bool = true var f float64 = 3.14 // Get Type information typeI := reflect.TypeOf(i) typeS := reflect.TypeOf(s) typeB := reflect.TypeOf(b) typeF := reflect.TypeOf(f) fmt.Printf("Variable 'i' Type: %v, Kind: %v\n", typeI, typeI.Kind()) fmt.Printf("Variable 's' Type: %v, Kind: %v\n", typeS, typeS.Kind()) fmt.Printf("Variable 'b' Type: %v, Kind: %v\n", typeB, typeB.Kind()) fmt.Printf("Variable 'f' Type: %v, Kind: %v\n", typeF, typeF.Kind()) // Demonstrating user-defined types and pointers type MyInt int var mi MyInt = 100 typeMI := reflect.TypeOf(mi) fmt.Printf("Variable 'mi' Type: %v, Kind: %v\n", typeMI, typeMI.Kind()) var ptrI *int = &i typePtrI := reflect.TypeOf(ptrI) fmt.Printf("Variable 'ptrI' Type: %v, Kind: %v\n", typePtrI, typePtrI.Kind()) fmt.Printf("Variable 'ptrI' Elem (dereferenced) Type: %v, Kind: %v\n", typePtrI.Elem(), typePtrI.Elem().Kind()) // Slices and Maps var slice []int typeSlice := reflect.TypeOf(slice) fmt.Printf("Variable 'slice' Type: %v, Kind: %v, Elem: %v\n", typeSlice, typeSlice.Kind(), typeSlice.Elem()) var m map[string]int typeMap := reflect.TypeOf(m) fmt.Printf("Variable 'm' Type: %v, Kind: %v, Key: %v, Elem: %v\n", typeMap, typeMap.Kind(), typeMap.Key(), typeMap.Elem()) }
Key reflect.Type
Methods:
Kind()
: Returns the fundamental kind of the type (e.g.,reflect.Int
,reflect.String
,reflect.Struct
,reflect.Ptr
,reflect.Slice
,reflect.Map
). This is the raw "classification" of the type in Go's internal type system.Name()
: Returns the type's name within its package. For built-in types, this is empty. ForMyInt
above, it would be "MyInt".String()
: Returns the string representation of the type.PkgPath()
: Returns the package path where the type was defined.Elem()
: If the type is a pointer, array, slice, or channel,Elem()
returns the element type. For maps, it returns the value type.NumField()
,Field(i)
: For structs, these methods allow iterating over the fields.NumMethod()
,Method(i)
: For types with methods, these allow inspecting callable methods.
The reflect.Value
Interface: Accessing and Modifying Values
reflect.Value
represents the runtime value of a variable. It provides methods to inspect, get, and potentially set the value.
To obtain a reflect.Value
, you use the reflect.ValueOf
function:
package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.14159 // Get Value information valueX := reflect.ValueOf(x) fmt.Printf("Variable 'x' Value: %v, Type: %v, Kind: %v\n", valueX, valueX.Type(), valueX.Kind()) // Accessing the concrete value from reflect.Value concreteValue := valueX.Float() // Specific method for float64 fmt.Printf("Concrete value of 'x': %f\n", concreteValue) // Attempting to set a value - will fail if not addressable // valueX.SetFloat(3.14) // Panic: reflect.Value.SetFloat using unaddressable value // To modify a value, the reflect.Value must be addressable and settable. // This means you need to pass a pointer to `reflect.ValueOf`. ptrX := &x valuePtrX := reflect.ValueOf(ptrX) fmt.Printf("Variable 'ptrX' Type: %v, Kind: %v\n", valuePtrX.Type(), valuePtrX.Kind()) fmt.Printf("Value pointed to by 'ptrX': %v\n", valuePtrX.Elem()) // To modify the original variable 'x', you need to use valuePtrX.Elem() // Elem() returns the reflect.Value that the pointer points to. // This returned Value is addressable and settable. if valuePtrX.Elem().CanSet() { valuePtrX.Elem().SetFloat(2.71828) fmt.Printf("New value of 'x' after reflection: %f\n", x) } else { fmt.Println("Cannot set value through reflection.") } // Example with a struct type Person struct { Name string Age int city string // Unexported field } p := Person{"Alice", 30, "New York"} vp := reflect.ValueOf(p) fmt.Printf("\nPerson struct value: %v\n", vp) // Accessing struct fields for i := 0; i < vp.NumField(); i++ { field := vp.Field(i) fieldType := vp.Type().Field(i) // Get the reflect.StructField to query name, tag etc. fmt.Printf("Field %d: Name=%s, Type=%v, Value=%v, CanSet=%t\n", i, fieldType.Name, field.Type(), field, field.CanSet()) // Unexported 'city' field cannot be set. } // Setting a struct field (requires a pointer to the struct for addressability) ptrP := &p vpMutable := reflect.ValueOf(ptrP).Elem() // Get the addressable reflect.Value of the struct itself if vpMutable.Kind() == reflect.Struct { nameField := vpMutable.FieldByName("Name") // Or by index: vpMutable.Field(0) if nameField.IsValid() && nameField.CanSet() { nameField.SetString("Bob") fmt.Printf("Name changed to: %s\n", p.Name) } else { fmt.Println("Cannot set Name field.") } // Attempt to set unexported field (will panic or CanSet will be false) // cityField := vpMutable.FieldByName("city") // if cityField.IsValid() && cityField.CanSet() { // Will be false // cityField.SetString("London") // } } }
Key reflect.Value
Methods:
Type()
: Returns thereflect.Type
of the value.Kind()
: Returns the fundamental kind of the value.Interface()
: Returns the value as aninterface{}
. This is how you get back the concrete value from areflect.Value
.CanSet()
: Returns true if the value can be changed. For areflect.Value
to be settable, it must be addressable and exported (for struct fields).SetFoo(...)
: Methods likeSetInt()
,SetFloat()
,SetString()
,SetBool()
are used to modify the underlying value.Elem()
: If the value represents a pointer, it returns thereflect.Value
that the pointer points to. If the value represents an interface, it returns thereflect.Value
of the concrete value stored in the interface.Field(i)
,FieldByName(name)
: For structs, these allow accessing individual fields.Call(args []reflect.Value)
: For functions or methods, this allows calling them dynamically.
Addressability and Settability
A crucial concept in Go reflection is addressability. A reflect.Value
is addressable if it corresponds to a variable that can be assigned to. In general, values obtained by reflect.ValueOf(x)
are not addressable because x
is passed by value. To make a value addressable via reflection, you must pass a pointer to reflect.ValueOf
. Then, you use Elem()
to get the reflect.Value
that the pointer points to.
Also, for struct fields, only exported fields (those starting with an uppercase letter) can be set via reflection across package boundaries. Unexported fields (city
in the Person
example) are not settable externally, even if the struct itself is addressable.
Practical Use Cases for Reflection
-
Serialization/Deserialization (e.g., JSON, YAML, Protocol Buffers): Reflection is at the heart of
encoding/json
and similar packages. They use reflection to iterate over struct fields, read their names (andjson:"tag"
annotations), and extract their values for serialization, or set values during deserialization.package main import ( "encoding/json" "fmt" ) type User struct { ID int `json:"id"` Name string `json:"full_name"` Email string `json:"-"` // Ignore this field Age int `json:"age,omitempty"` // Omit if zero } func main() { u := User{ID: 1, Name: "Alice Smith", Email: "alice@example.com"} data, _ := json.Marshal(u) fmt.Println(string(data)) // {"id":1,"full_name":"Alice Smith"} var u2 User json.Unmarshal(data, &u2) fmt.Printf("Unmarshal: %+v\n", u2) }
The
encoding/json
package usesreflect.Type
to read the struct field names and tags, andreflect.Value
to get/set field values. -
ORM/Database Drivers: ORMs use reflection to map database table columns to struct fields, and to assign values from query results back to Go struct instances.
-
Validation Libraries: A common pattern for validation is to define validation rules using struct tags. Reflection can then be used to read these tags and apply validation logic to the corresponding fields.
package main import ( "fmt" "reflect" "strconv" ) type UserProfile struct { Username string `validate:"required,min=5,max=20"` Email string `validate:"required,email"` Age int `validate:"min=18,max=120"` } func Validate(s interface{}) error { val := reflect.ValueOf(s) if val.Kind() == reflect.Ptr { val = val.Elem() } if val.Kind() != reflect.Struct { return fmt.Errorf("validation can only be performed on structs") } typ := val.Type() for i := 0; i < val.NumField(); i++ { field := val.Field(i) fieldType := typ.Field(i) tag := fieldType.Tag.Get("validate") if tag == "" { continue } tags := splitTags(tag) // Simple split for demonstration for _, t := range tags { switch { case t == "required": // Check for zero value if reflect.DeepEqual(field.Interface(), reflect.Zero(field.Type()).Interface()) { return fmt.Errorf("%s is required", fieldType.Name) } case t == "email": if !isValidEmail(field.String()) { return fmt.Errorf("%s is not a valid email", fieldType.Name) } case FieldStartsWith("min=", t): minValStr := t[4:] minVal, _ := strconv.Atoi(minValStr) // Error handling omitted for brevity if field.Kind() == reflect.Int && field.Int() < int64(minVal) { return fmt.Errorf("%s must be at least %d", fieldType.Name, minVal) } if field.Kind() == reflect.String && len(field.String()) < minVal { return fmt.Errorf("%s must have a minimum length of %d", fieldType.Name, minVal) } // ... other validation rules } } } return nil } func splitTags(tag string) []string { // In a real validator, you'd parse this more robustly return []string{"required", "min=5", "max=20"} // Dummy for illustration } func isValidEmail(email string) bool { // Basic check, use a proper regex for production return len(email) > 5 && contains(email, "@") } func contains(s, substr string) bool { return len(s) >= len(substr) && s[len(s)-len(substr):] == substr || s[:len(substr)] == substr } func FieldStartsWith(prefix, field string) bool { return len(field) >= len(prefix) && field[:len(prefix)] == prefix } func main() { user1 := UserProfile{Username: "testuser", Email: "test@example.com", Age: 25} if err := Validate(user1); err != nil { fmt.Printf("Validation Error for user1: %v\n", err) } else { fmt.Println("User1 validated successfully.") } user2 := UserProfile{Username: "bad", Email: "invalid", Age: 10} if err := Validate(user2); err != nil { fmt.Printf("Validation Error for user2: %v\n", err) // Example output depends on actual splitTags/isValidEmail } else { fmt.Println("User2 validated successfully.") } }
When to Use Reflection (and When Not To)
When to Use:
- Generic Programming: When you need to write code that works with arbitrary types without knowing them at compile time (e.g., generic serialization, database tooling, dependency injection).
- Runtime Type Inspection: When you need to inspect an object's type, fields, or methods dynamically (e.g., custom marshalers, debuggers).
- Struct Tags Processing: Reading and interpreting struct tags for configuration, validation, etc.
When Not To Use (or Use with Caution):
-
Performance Sensitive Code: Reflection is generally slower than direct type manipulation. Each
reflect.Value
andreflect.Type
operation involves some overhead. Avoid it in tight loops or performance-critical paths if a compile-time alternative exists. -
Encouraging Fragile Code: Over-reliance on reflection can lead to code that is harder to read, debug, and refactor. Changes to struct fields (renaming, reordering, removing) can break reflection-based code that uses field indices, though
FieldByName
is more robust. -
Simple Type Checking: If you only need to check if a variable implements a certain interface, use a type assertion (
v.(MyInterface)
). If you need to check the concrete type, a type switch (switch v.(type)
) is often more idiomatic and performant.// Instead of: // func process(i interface{}) { // if reflect.TypeOf(i).Kind() == reflect.Int { ... } // } // Prefer: func process(i interface{}) { switch v := i.(type) { case int: fmt.Printf("It's an int: %d\n", v) case string: fmt.Printf("It's a string: %s\n", v) default: fmt.Printf("Unknown type: %T\n", v) } }
Conclusion
Go's reflection capabilities, provided by the reflect
package, offer a powerful way to introspect and manipulate program elements at runtime. Understanding reflect.Type
for type information and reflect.Value
for value manipulation, along with the concepts of addressability and settability, unlocks the ability to build flexible and generic Go libraries. While reflection comes with a performance overhead and potential for more fragile code, it is an indispensable tool for tasks that require dynamic interaction with Go's type system, particularly in serialization, ORMs, and validation. Used judiciously, reflection enhances the versatility and expressiveness of your Go applications.