Go Reflection: When and How to Use reflect
Reviewer’s note: This tutorial explains what Go reflection is, the three laws that govern it, and how to use
reflectfor the few problems that need it: reading struct tags, setting values, and calling methods by name. Every example was tested on Go 1.24.7 with observed output, including a panic, and reviewed against Go 1.26.5. By the end you can build a tag-driven validator and know when to reach for reflection and when not to.
Works with Go 1.18+ (all examples tested on Go 1.24.7). If Go is new to you, start with the complete Go tutorial first, since this article assumes you are comfortable with structs, methods, and interfaces. The type-system guide provides the most useful background.
Why you rarely need reflection
Reflection lets a program inspect and manipulate its own types and values at runtime. It sounds powerful, and that is exactly the problem: it is the tool people reach for too early.
Before you type import "reflect", ask whether an interface or generics solves the same problem with compile-time checking. Most “I need to handle any type” situations do not need reflection:
- If you need one function to work with several types that share behavior, define an interface. The compiler verifies every implementation.
- If you need one function to work with several types that share structure (a min over any ordered type, a generic cache), use generics. Again, the compiler checks you.
Reflection throws that safety away. Code that goes through reflect is checked at runtime, not compile time, so a mistake that the compiler would have caught becomes a panic in production. It is also slower, harder to read, and invisible to tools like “find all callers”. The Go blog’s Laws of Reflection opens by calling reflection “a powerful tool that should be avoided unless strictly necessary.” Treat that as the default.
So when is it necessary? When you genuinely do not know the types at compile time: a JSON encoder that must serialize any struct a caller hands it, an ORM mapping database rows onto arbitrary structs, a validation library reading tags off types it has never seen. Those libraries exist so that your application code does not have to touch reflect at all.
The three laws of reflection
The whole package rests on three rules, laid out in the Go blog. Learn these and the API stops feeling like a bag of unrelated functions.
- Reflection goes from an interface value to a reflection object.
reflect.TypeOfandreflect.ValueOftake aninterface{}(any value) and hand back areflect.Typeand areflect.Valuedescribing it. - Reflection goes from a reflection object back to an interface value.
Value.Interface()reverses step one, returning ananyyou type-assert back to a concrete type. - To modify a reflection object, the value must be settable. You can change a value through reflection only if the
reflect.Valueis addressable and was obtained in a way that preserves that addressability. Otherwise the write panics.
Here are the first two laws in code, the round trip in and back out:
package main
import (
"fmt"
"reflect"
)
func main() {
var price float64 = 19.99
// Law 1: interface value -> reflection object
v := reflect.ValueOf(price)
t := reflect.TypeOf(price)
fmt.Printf("type: %v, kind: %v, value: %v\n", t, v.Kind(), v.Float())
// Law 2: reflection object -> interface value
back := v.Interface().(float64)
fmt.Printf("recovered: %v\n", back)
}
Output:
type: float64, kind: float64, value: 19.99
recovered: 19.99
The third law is where reflection gets subtle, and it gets its own section below, because the panic it produces catches almost everyone once.
reflect.TypeOf and reflect.ValueOf
These two functions are the front door. TypeOf returns a reflect.Type (what the value is), ValueOf returns a reflect.Value (the value itself, wrapped so you can inspect and manipulate it).
package main
import (
"fmt"
"reflect"
)
func main() {
orderTotal := 4999
customerName := "Dana Ito"
prices := []float64{9.99, 19.99}
fmt.Println(reflect.TypeOf(orderTotal), reflect.ValueOf(orderTotal))
fmt.Println(reflect.TypeOf(customerName), reflect.ValueOf(customerName))
fmt.Println(reflect.TypeOf(prices), reflect.ValueOf(prices))
}
Output:
int 4999
string Dana Ito
[]float64 [9.99 19.99]
Both functions take interface{}, so any value goes in. The moment you call them you have crossed from the statically typed world into the runtime one. To read the value out, you use typed accessors on the reflect.Value: Int(), Float(), String(), Bool(), and so on. Call the wrong one (Float() on a value holding an int) and it panics, which is the recurring theme of this package: mistakes surface at runtime.
Kind vs Type: the distinction that trips people up
Type is the specific, named type. Kind is the underlying category from a fixed set (Int, Float64, Slice, Struct, Ptr, Map, and so on). A named type built on float64 has a Type of main.Celsius but a Kind of float64.
package main
import (
"fmt"
"reflect"
)
type Celsius float64
func main() {
var temp Celsius = 21.5
v := reflect.ValueOf(temp)
fmt.Println("Type:", v.Type()) // the named type
fmt.Println("Kind:", v.Kind()) // the underlying category
fmt.Println(reflect.TypeOf([]int{}).Kind())
fmt.Println(reflect.TypeOf([3]int{}).Kind())
fmt.Println(reflect.TypeOf(map[string]int{}).Kind())
}
Output:
Type: main.Celsius
Kind: float64
slice
array
map
Why it matters: you almost always switch on Kind, not Type. There are infinitely many types (every struct anyone defines) but a small, fixed set of kinds. A serializer asks “is this a struct, a slice, or a scalar?” (a Kind question) far more often than “is this exactly main.Celsius?” (a Type question). Getting these two confused is the most common early reflection bug.
Inspecting structs: iterating fields and reading tags
This is a practical use of reflection. A struct’s fields and tags are the metadata a serializer or validator needs, and there is no way to read them generically without reflection.
reflect.Type gives you NumField() and Field(i); the matching reflect.Value gives you Field(i) for the value. Each StructField carries a Tag, and Tag.Get("key") parses the standard key:"value" tag format for you.
package main
import (
"fmt"
"reflect"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
admin bool // unexported
}
func main() {
u := User{ID: 1042, Name: "Dana Ito", Email: "[email protected]"}
t := reflect.TypeOf(u)
v := reflect.ValueOf(u)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
jsonTag := field.Tag.Get("json")
fmt.Printf("%-6s kind=%-7s json=%-16q exported=%t value=%v\n",
field.Name, value.Kind(), jsonTag, field.IsExported(), safe(value))
}
}
// safe returns the value only if reflection is allowed to read it.
// Unexported fields cannot be handed back out as an interface value.
func safe(v reflect.Value) any {
if v.CanInterface() {
return v.Interface()
}
return "<unexported>"
}
Output:
ID kind=int json="id" exported=true value=1042
Name kind=string json="name" exported=true value=Dana Ito
Email kind=string json="email,omitempty" exported=true [email protected]
admin kind=bool json="" exported=false value=<unexported>
Notice two production-relevant details. The Email tag is email,omitempty: tag values often carry comma-separated options, so you split on comma to read the name and the flags separately. And admin is unexported, so CanInterface() is false and you cannot pull its value out through reflection. That is the runtime enforcement of Go’s export rules, and it is why every reflective library only touches exported fields.
This is not a toy. It is precisely how encoding/json decides output keys. The same struct through json.Marshal:
u := User{ID: 1042, Name: "Dana Ito"}
out, _ := json.Marshal(u) // error handling omitted for brevity
fmt.Println(string(out))
Output:
{"id":1042,"name":"Dana Ito"}
json read the json:"id" and json:"name" tags with the same Tag.Get mechanism, applied omitempty to drop the empty Email, and skipped the unexported admin, exactly as your loop above observed. When you understand the field-and-tag loop, you understand what every struct-tag library in Go is doing underneath.
Setting values: addressability and settability
The third law in action. You cannot change a value through a reflect.Value obtained directly from reflect.ValueOf, because that value is a copy, and copies are not addressable. Try it and you get a panic:
package main
import (
"fmt"
"reflect"
)
func main() {
price := 19.99
v := reflect.ValueOf(price)
fmt.Println("CanSet:", v.CanSet())
v.SetFloat(24.99) // panics
}
Output:
CanSet: false
panic: reflect: reflect.Value.SetFloat using unaddressable value
goroutine 1 [running]:
reflect.flag.mustBeAssignableSlow(0x0?)
/usr/local/go1.24.7/src/reflect/value.go:260 +0x74
...
exit status 2
reflect.ValueOf(price) received a copy of price (arguments are passed by value in Go, and the parameter is interface{}). Even if reflection let you write to it, you would be writing to a copy nobody can see. So it refuses. CanSet() returns false, and SetFloat panics.
The fix is to hand reflection something addressable: a pointer, then step through it with Elem(). Elem() on a pointer Value returns the Value it points at, and that one is addressable because it names a real memory location.
package main
import (
"fmt"
"reflect"
)
func main() {
price := 19.99
v := reflect.ValueOf(&price).Elem() // pointer, then dereference
fmt.Println("CanSet:", v.CanSet())
v.SetFloat(24.99)
fmt.Println("price is now:", price)
}
Output:
CanSet: true
price is now: 24.99
Now CanSet() is true and the write reaches the real price. The rule to memorize: to set through reflection, start from a pointer and call Elem(). Two more conditions come with it. The field must also be exported (reflection will not let you set unexported fields even when addressable), and you must use the setter that matches the kind (SetFloat, SetString, SetInt). This pointer-then-Elem pattern is how a JSON decoder or an ORM writes decoded values back into your struct.
Calling methods by name
Reflection can look up a method by its string name and call it. This is how dependency-injection frameworks and some RPC layers dispatch to handlers they discover at runtime.
Value.MethodByName("Name") returns a Value representing the method (bound to its receiver). You call it with Call, passing a []reflect.Value of arguments and getting a []reflect.Value of results.
package main
import (
"fmt"
"reflect"
)
type Invoice struct {
AmountCents int
}
func (in Invoice) WithTax(ratePercent float64) int {
return in.AmountCents + int(float64(in.AmountCents)*ratePercent/100)
}
func main() {
inv := Invoice{AmountCents: 10000}
v := reflect.ValueOf(inv)
method := v.MethodByName("WithTax")
if !method.IsValid() {
fmt.Println("no such method")
return
}
args := []reflect.Value{reflect.ValueOf(8.5)}
results := method.Call(args)
fmt.Println("total with tax:", results[0].Int())
}
Output:
total with tax: 10850
Always check IsValid(): a typo in the method name does not fail at compile time (the name is a string), it returns an invalid Value, and calling that panics. Same for argument count and types: mismatches surface only when Call runs. This is reflection’s whole tradeoff in one example. You gain the ability to dispatch on a name you did not know at compile time, and you lose every check the compiler would normally give you.
The performance cost, measured
Reflection is slower than direct field access, and it is worth knowing by how much before you put it on a hot path. Here is a benchmark comparing a direct field read against the reflective equivalent:
package main
import (
"reflect"
"testing"
)
type Account struct {
Balance int
}
var sink int
func BenchmarkDirect(b *testing.B) {
a := Account{Balance: 500}
for i := 0; i < b.N; i++ {
sink = a.Balance
}
}
func BenchmarkReflect(b *testing.B) {
a := Account{Balance: 500}
v := reflect.ValueOf(a)
for i := 0; i < b.N; i++ {
sink = int(v.Field(0).Int())
}
}
Output of go test -bench=. -benchmem:
goos: linux
goarch: amd64
cpu: Intel(R) Xeon(R) Processor @ 2.10GHz
BenchmarkDirect-2 1000000000 0.2175 ns/op 0 B/op 0 allocs/op
BenchmarkReflect-2 423716439 4.198 ns/op 0 B/op 0 allocs/op
The direct read is about 0.22 ns; the reflective read is around 4 ns, roughly 15 to 20 times slower (the reflect number wobbles between runs, the direct one does not). Both avoid allocation here, but many real reflection paths do allocate, especially Interface() and Call, which box values into interfaces. A single reflective read is cheap in absolute terms, so the cost only matters when it runs in a tight loop. The practical rule: use reflection at the edges (once per request to decode a body, once per struct to build a mapping), and never in the innermost loop of a hot code path. Libraries that care, like high-performance JSON packages, cache the reflective analysis of each type the first time they see it and reuse it.
Building a tag-driven struct validator
Here is reflection earning its place: a small validator that reads a validate:"required" tag and reports fields left at their zero value. This is the exact shape of libraries like go-playground/validator, minus the hundred other rules.
The design uses everything above. Accept any, handle both a struct and a pointer to one (via Elem()), switch on Kind to reject non-structs, loop the fields, read the tag, and use IsZero() to detect a missing required value.
package main
import (
"fmt"
"reflect"
"strings"
)
// Validate checks every exported field tagged `validate:"required"`.
// A field is invalid when required and equal to its zero value.
func Validate(s any) []error {
var problems []error
v := reflect.ValueOf(s)
// Accept both a struct and a pointer to a struct.
if v.Kind() == reflect.Pointer {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return []error{fmt.Errorf("validate: expected a struct, got %s", v.Kind())}
}
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
rules := field.Tag.Get("validate")
if rules == "" {
continue
}
for _, rule := range strings.Split(rules, ",") {
if rule == "required" && v.Field(i).IsZero() {
problems = append(problems,
fmt.Errorf("field %q is required", field.Name))
}
}
}
return problems
}
type SignupRequest struct {
Username string `validate:"required"`
Email string `validate:"required"`
Age int // no rule, optional
}
func main() {
valid := SignupRequest{Username: "dana", Email: "[email protected]", Age: 33}
invalid := SignupRequest{Username: "dana"} // missing Email
fmt.Println("valid:", Validate(valid))
fmt.Println("invalid:", Validate(invalid))
}
Output:
valid: []
invalid: [field "Email" is required]
Fifty lines, and any struct in your codebase can now declare its required fields in tags and be checked by one function that has never heard of SignupRequest. That decoupling (rules on the type, engine that knows nothing about the type) is why reflection-based validators are worth their cost: the alternative is a hand-written Validate method on every struct, duplicated and drifting. Note that IsZero() treats an empty string, a zero int, and a nil pointer all as “missing”, which is usually what a required check wants, but it is a design choice you should be aware of.
When reflection is the right tool
Reach for reflection when the type is genuinely unknown at compile time and the work is fundamentally about type structure:
- Generic serializers and codecs.
encoding/json,encoding/xml, and their faster third-party cousins must handle any struct. There is no other way to walk arbitrary fields and tags. - ORMs and query builders. Mapping database columns onto struct fields by tag is the same field-and-tag loop you saw above.
- Validation libraries. Exactly the validator you just built, scaled up.
- Dependency injection and RPC dispatch. Wiring components or routing a method name to a handler discovered at runtime.
What these share: they are libraries, written once, that let thousands of callers avoid reflection entirely. That is the tell. If you are writing a library that must operate on types its users define, reflection may be correct. If you are writing application code and reaching for reflect, stop and look again, because an interface or a generic almost certainly fits better and keeps the compiler on your side.
Common mistakes with Go reflection
Using reflection where interfaces or generics suffice
The most expensive mistake, because it is architectural. A function that switches on reflect.Kind to handle a few known types should almost always be an interface or a generic function instead. Reflection here trades away compile-time safety, speed, and readability for nothing. Ask “do I actually not know these types at compile time?” If you can list them, you do know them, and you do not need reflection.
Panicking on an unsettable value
v := reflect.ValueOf(config) // a copy
v.Field(0).SetString("prod") // panic: using unaddressable value
You cannot set through a Value that came from reflect.ValueOf of a plain value. Pass a pointer and call Elem() first, as in the settability section. Always guard writes with CanSet() when the input might not be addressable.
Ignoring the performance cost and reflecting in hot paths
A reflective field read is 15 to 20 times slower than a direct one, and reflective calls often allocate. One reflection per request is invisible; one per element in a million-row loop is a profiler finding. Do the reflective analysis once (build a field map, cache it by type) and reuse it, the way mature libraries do. Never leave reflect.ValueOf inside your tightest loop.
Not handling pointer vs value
v := reflect.ValueOf(s)
t := v.Type()
t.Field(0) // panics if s is a *SignupRequest, not a SignupRequest
If a caller passes a pointer, Kind() is Pointer and NumField() panics because a pointer has no fields. Normalize first: if v.Kind() == reflect.Pointer { v = v.Elem() }, then check v.Kind() == reflect.Struct before touching fields. Every robust reflective function starts with this dance.
Forgetting that unexported fields are off limits
Reflection can see unexported fields (NumField counts them) but cannot read their values out with Interface() or set them. CanInterface() and CanSet() both return false. Skip unexported fields (field.IsExported()) rather than letting a later Interface() panic.
What next
Reflection sits on top of Go’s type system, so the productive next steps go both deeper and sideways:
- Nearly every reflection problem has a better non-reflective answer. Learn when Go generics give you the same flexibility with full compile-time checking, and when interfaces are the cleaner fit.
- The struct-tag loop here is exactly how Go’s encoding/json package marshals and unmarshals. Seeing it applied end to end makes both topics click.
- Reflection reads and writes fields through addresses, so revisit the type-system guide if the pointer and
Elem()mechanics felt shaky. - If any of the fundamentals here felt fast, go back to the complete Go tutorial and work forward.