Runtime Type Introspection
In a statically typed language like Go, variables have a fixed type known at compile time. The compiler uses this information to generate efficient machine code — it knows exactly how many bytes a User struct occupies and where each field lives in memory.
But what if you are writing a function like json.Marshal? It needs to accept any struct, inspect its fields, read their values, and examine struct tags — all without knowing the struct definition at compile time. This is the problem reflection solves.
Reflection (the reflect package) allows a running program to examine its own structure at runtime. There are two core types:
reflect.Type: Describes what something is — its kind (struct, slice, map, int), its name, its fields, and their tags. Think of it as the blueprint.reflect.Value: Describes what is inside something — the actual data stored in a variable at this moment. Think of it as the contents.
package main
import (
"fmt"
"reflect"
)
type Server struct {
Host string
Port int
}
func inspect(x interface{}) {
t := reflect.TypeOf(x)
v := reflect.ValueOf(x)
fmt.Printf("Type name: %s\n", t.Name()) // "Server"
fmt.Printf("Kind: %s\n", t.Kind()) // "struct"
fmt.Printf("Num fields: %d\n", t.NumField())
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
fmt.Printf(" Field %s = %v\n", field.Name, value)
}
}
func main() {
s := Server{Host: "localhost", Port: 8080}
inspect(s)
}
Expected output:
Type name: Server
Kind: struct
Num fields: 2
Field Host = localhost
Field Port = 8080
The interface{} parameter (or any in Go 1.18+) is the entry point for reflection. Every value in Go can be assigned to an interface{}, and from there, reflect.TypeOf and reflect.ValueOf can unwrap it. This is why almost every reflection-based function accepts interface{} as its argument — it is the universal container that lets reflection get started.
Interviewers sometimes ask: "How does encoding/json handle arbitrary structs?" The answer is reflection. json.Marshal calls reflect.TypeOf to discover field names and struct tags (like `json:"host"`), then calls reflect.ValueOf to read the actual field values. A strong follow-up is to mention that the standard library caches the reflected type information to avoid re-doing the expensive lookup on every call — this is why sync.Map or similar caching structures appear in serialization library internals.