Reflection is the ability for a program to inspect — and sometimes modify — its own structure at runtime. It’s how libraries like encoding/json know which fields a struct has without you telling them, and how testing libraries can compare deeply nested values.
Reflection is powerful, slow, and easy to misuse. Most Go code never touches it. But knowing how it works is essential for understanding parts of the standard library and many popular packages.
Two key types
The reflect package gives you two main types:
reflect.Type— describes a Go type (its name, fields, methods, etc.)reflect.Value— wraps a Go value, lets you inspect (and sometimes set) it
You get them with reflect.TypeOf and reflect.ValueOf.
package main
import (
"fmt"
"reflect"
)
func main() {
var x = 42
var s = "hello"
fmt.Println(reflect.TypeOf(x), reflect.ValueOf(x))
fmt.Println(reflect.TypeOf(s), reflect.ValueOf(s))
}
int 42
string hello
Inspecting a struct
Where reflection earns its keep is with structs. You can list fields, read tags, and access values.
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age,omitempty"`
}
func main() {
u := User{Name: "Mani", Email: "[email protected]", Age: 30}
t := reflect.TypeOf(u)
v := reflect.ValueOf(u)
fmt.Println("Type:", t.Name())
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
jsonTag := field.Tag.Get("json")
fmt.Printf("%s (%s) [json=%q] = %v\n", field.Name, field.Type, jsonTag, value)
}
}
Type: User
Name (string) [json="name"] = Mani
Email (string) [json="email"] = [email protected]
Age (int) [json="age,omitempty"] = 30
Walk through it:
t.NumField()returns the number of fieldst.Field(i)returns metadata about fieldi(name, type, tag)v.Field(i)returns the value of fieldifield.Tag.Get("json")reads the struct tag — the\json:”…”“ annotation written next to the field
Struct tags are how reflection-based libraries — JSON encoders, ORMs, validation libraries — get configuration without you writing extra code.
A simple use case: JSON-like field listing
This pattern is the basis for libraries like encoding/json. A simplified version:
package main
import (
"fmt"
"reflect"
"strings"
)
func describe(v any) {
t := reflect.TypeOf(v)
val := reflect.ValueOf(v)
if t.Kind() != reflect.Struct {
fmt.Println("not a struct")
return
}
var lines []string
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := val.Field(i)
lines = append(lines, fmt.Sprintf(" %q: %v", field.Tag.Get("json"), value))
}
fmt.Println("{\n" + strings.Join(lines, ",\n") + "\n}")
}
type Product struct {
Name string `json:"name"`
Price float64 `json:"price"`
}
func main() {
p := Product{Name: "Pen", Price: 1.25}
describe(p)
}
{
"name": Pen,
"price": 1.25
}
This isn’t real JSON (we’d need to quote strings, escape characters, etc.) — but it’s the kind of work that real JSON encoders do under the hood, generalized to any struct.
Modifying values with reflection
Reflection can also set values, but with a catch: you need to pass a pointer to the value, and call Elem() to get the underlying value.
package main
import (
"fmt"
"reflect"
)
type Config struct {
Host string
Port int
}
func main() {
c := &Config{Host: "localhost", Port: 8080}
v := reflect.ValueOf(c).Elem()
v.FieldByName("Host").SetString("example.com")
v.FieldByName("Port").SetInt(443)
fmt.Println(c.Host, c.Port)
}
example.com 443
The Elem() is the key — reflect.ValueOf(c) gives you the pointer; .Elem() follows the pointer to the actual struct, where you can modify fields.
Reflection-based modification only works on exported fields (ones with capital first letters). Trying to set an unexported field via reflection panics.
When NOT to use reflection
You should reach for reflection only when:
- You’re writing a library that has to handle arbitrary types (JSON encoder, validator, ORM)
- You need to inspect struct tags or method names dynamically
- There’s no compile-time alternative — generics, interfaces — that fits
For application code, prefer interfaces and generics. Reflection is:
- Slow — orders of magnitude slower than direct access
- Loose — type checks happen at runtime, defeating Go’s static type system
- Hard to read —
reflect.ValueOf(x).Elem().FieldByName("Y")is not friendly
A good rule: if you can solve a problem with an interface, use an interface. If you can solve it with generics, use generics. Reach for reflection only when those tools won’t fit.
Standard library uses of reflection
Knowing where reflection is used helps you understand it in context:
encoding/jsonuses reflection to read struct tags and walk fieldsencoding/xml,encoding/gob— same idea, different formatsfmtuses reflection for%vand friends to format any valuereflect.DeepEqualuses reflection to compare nested structures- Popular external packages —
gorm,validator,testify— all rely on reflection
What’s next
The last lesson covers generics — Go’s relatively recent feature for writing code that’s type-safe across many types. In many cases, generics are the right answer where you might have once reached for reflection.