Search and navigate

Pages

Start HereRoadmapTutorialsCheatsheetInterview QuestionsResources

Published tutorials

Building a REST API with Go: The Complete TutorialError Handling in Go: Wrapping, Classification, and APIsFan-Out Fan-In in Go: Parallel Work with ChannelsFuzz Testing in Go: Finding Bugs AutomaticallyGeneric Data Structures in Go: Stack, Queue and SetGeneric Functions in Go: Writing Reusable CodeGin in Go: Build and Test a Small JSON APIGo Benchmarking: Measuring Performance with testing.BGo Channel Patterns: Advanced Recipes That WorkGo Channels: Communication Between GoroutinesGo Concurrency Patterns: A Practical CatalogueGo Constraints: comparable, Ordered and Custom SetsGo Context: Cancellation, Deadlines, and Request ValuesGo defer: Cleanup, Ordering, and Common TrapsGo HTTP Client: Timeouts, JSON, and Reliable RequestsGo HTTP Middleware: Build a Production ChainGo Interview Questions: 25 Answers That Show UnderstandingGo Mutex: Protecting Shared State from Race ConditionsGo net/http: Build an HTTP Server from ScratchGo Reflection: When and How to Use reflectGo select Statement: Multiplexing Channels ExplainedGo Tools You Need for a Reliable Development LoopGo Tutorial: From First Program to Backend FoundationsGo Type Parameters: Syntax and Constraints ExplainedGo Worker Pool Pattern: Bounded Concurrency at ScaleGo's Garbage Collector Explained: How Memory Is ManagedGo's Type System Deep Dive: Static Typing Done RightGolang Compilation and Execution ExplainedGolang Developer Salary 2026: US, UK and RemoteGolang File Structure for Modules and ApplicationsGolang Hello World and Your First Go ProgramGolang IDEs: Choose an Editor for Your WorkflowGolang Interfaces: What They Are and How to Use ThemGoroutine Leaks in Go: How to Detect and Prevent ThemGoroutines in Go: Concurrency Without LeaksHow to Download and Install Golang SafelyHow to Learn Golang with a Focused Study PlanIntegration Testing in Go: Testing Real BoundariesJSON in Go: Encoding, Decoding, and ValidationLearn Golang with a Practical Beginner CourseMocking in Go: Test Doubles Without Magicsync.WaitGroup in Go: Coordinating Multiple GoroutinesTable-Driven Tests in Go: The Idiomatic WayTesting in Go: Table Tests, HTTP, Fakes, and CoverageWhat Is Go and How Does a Go Program Work?Why Learn Golang and What Is Go Used For?Writing Unit Tests in Go: A Practical Guide
Open full search and filters →

Go Reflection: When and How to Use reflect

Learn Go reflection the right way: the three laws, reflect.TypeOf and ValueOf, struct tags, settability, a tested validator, and when to avoid reflect.

Standard-library lessonGo requirement: Go 1.18+ (all examples tested on Go 1.24.7)How tutorials are checked
Generics · Lesson 7Saved in this browser. No account required.
A Go value branching into runtime type information and inspectable struct fields
Reflection exposes a value's runtime type, kind, fields, and tags. Image: Golang Tutorial

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 reflect for 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.

  1. Reflection goes from an interface value to a reflection object. reflect.TypeOf and reflect.ValueOf take an interface{} (any value) and hand back a reflect.Type and a reflect.Value describing it.
  2. Reflection goes from a reflection object back to an interface value. Value.Interface() reverses step one, returning an any you type-assert back to a concrete type.
  3. To modify a reflection object, the value must be settable. You can change a value through reflection only if the reflect.Value is 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.