AlgoMaster Logo

Reflection Basics

Last Updated: May 22, 2026

Medium Priority
10 min read

Reflection is the ability of a program to inspect and modify its own values at runtime, even when their static types aren't known at compile time. Go's reflect package is what makes this possible. This chapter explains what reflection is, why it exists, the three rules that govern how it works, and when to use it instead of plain interfaces or generics.

What Reflection Is

Go is a statically typed language. Every variable has a type that the compiler knows about, and the compiler refuses to let you do something nonsensical, like add a string to an int. That static type system is one of the reasons Go programs are fast and predictable.

Reflection is the escape hatch. It's a set of APIs that let your code look at a value without knowing its type at compile time, ask "what type is this?", "what fields does it have?", "what methods does it expose?", and even change the value if certain conditions are met. The code runs at runtime, not compile time, so the compiler can't help you catch mistakes the way it normally would.

The standard library packages that handle JSON, XML, database scanning, and template rendering all use reflection internally. When you call json.Marshal(myStruct), the JSON package doesn't know the type of myStruct ahead of time. It uses reflection to discover the struct's fields, read their tags, pull their values, and turn them into bytes. Without reflection, you'd have to write a custom encoder for every type by hand.

The diagram shows the basic shape. You hand a value to reflect, and you get back two kinds of information: metadata about its type (what kind of thing it is, what fields or methods it has) and a handle to the value itself (which you can read from and, under certain conditions, write to).

Here's the smallest useful example. Suppose we want a helper that prints any value's type and dynamic kind, no matter what's passed in.

The describe function accepts any (an empty interface), so the caller can pass anything. Inside, reflect.TypeOf returns a reflect.Type that holds the runtime type information. t.Kind() collapses that into one of a fixed set of categories like int, string, slice, struct. One function handled four totally different types without knowing any of them at compile time.

Why Reflection Exists

A static type system is excellent at catching mistakes when the types are known in advance. The problem is that not every program knows its types in advance. Encoders, decoders, ORMs, dependency injection containers, template engines, and test frameworks all share one trait: they work with arbitrary user types they've never seen before.

Consider encoding/json. The signature of Marshal is func Marshal(v any) ([]byte, error). The JSON encoder has no idea what shape v is when you call it. It might be a struct with three fields, or a slice of structs, or a map of string to int. The encoder still has to produce valid JSON for any of those. The only way to do that without writing a separate encoder per type is to inspect v at runtime, walk its fields, and emit JSON for each.

The same pattern shows up in database/sql, where rows.Scan(&dest) takes pointers to destination variables of any type and fills them in from a SQL result. The driver has no compile-time knowledge of what dest is. It uses reflection to figure out what kind of pointer it received and how to write into it.

PackageWhat reflection does for it
encoding/jsonWalks struct fields, reads tags, encodes any value to JSON
encoding/xmlSame idea, for XML output
database/sqlWrites query results into pointer destinations of arbitrary types
text/template, html/templateLooks up named fields and methods on any value passed to the template
fmtFormats any value with %v, including printing struct field names with %+v
testingCompares deeply nested structures in reflect.DeepEqual

So reflection exists because some problems genuinely need it. Without reflect, every one of those packages would have to push the work onto you: you'd write a MarshalJSON method for every type, or a ScanInto method for every struct that talks to the database. Go's authors chose to provide reflection so that one library can serve every user-defined type without per-type boilerplate.

There's a real cost to this, though. Reflection skips the compiler's type checks, so a mistake (passing the wrong kind of pointer, accessing a field that doesn't exist) becomes a runtime panic instead of a build error. Reflective code is also slower than direct code: the runtime has to look up type information, check kinds, allocate boxes for values.

The reflect Package at a High Level

The reflect package gives you three things that matter for everyday use: a Type value that describes a Go type, a Value that wraps a Go value plus its type, and a Kind enum that categorizes types into broad buckets like Int, Struct, Slice, Map, Ptr, Interface, and so on.

reflect.TypeOf(v) returns a reflect.Type describing the dynamic type of v. reflect.ValueOf(v) returns a reflect.Value wrapping the value itself. Together they're the entry points to the entire reflection API. Almost every reflective program in Go starts with one of these two calls.

The Type answers questions about the type (its name, its kind, its package path, its fields if it's a struct). The Value lets you read the underlying data through typed accessors like Float, Int, String. The two stay in sync: reflect.ValueOf(v).Type() returns the same Type that reflect.TypeOf(v) would.

These are the two doors into the reflect package, and almost every reflection task starts by walking through one of them.

The Three Laws of Reflection

Rob Pike's article "The Laws of Reflection" lays out three rules that explain how reflection in Go connects to the static type system. They sound abstract on paper, so we'll walk through each one with code.

Law 1: Reflection Goes From Interface Value to Reflection Object

Every reflective call starts with an interface{} (now spelled any). When you pass a concrete value like 42 or "Notebook" to a function that accepts any, Go wraps it in an interface value automatically. reflect.TypeOf and reflect.ValueOf both accept any, so the first thing that happens is this implicit boxing.

price is a float64 in the source code. Once it's passed to reflect.TypeOf, Go has boxed it into an any, and reflect.TypeOf extracts the dynamic type information from that interface. The same happens for reflect.ValueOf. So the first law is simply: reflection objects come from interface values, and interface values are how the language smuggles a concrete value plus its type across a function boundary.

Law 2: Reflection Goes From Reflection Object Back to Interface Value

Once you have a reflect.Value, you can convert it back to an any with the Interface() method. From there a type assertion gets you the original concrete value.

The round trip preserves the type. The value comes in as float64, gets boxed into an interface, becomes a reflect.Value, and turns back into the same float64 at the end. This matters because libraries that do reflective work often need to hand a value back to user code, and Interface() is how they do it.

The second law also explains why reflect.Value.Interface() returns any, not the concrete type. The reflect package doesn't know your concrete type at compile time. It can only give you back an any, and the type assertion is your way of saying "I know what's in there".

Law 3: To Modify a Reflection Object, the Value Must Be Settable

This is the trickiest law. Most first attempts to use reflection to change a value fail here.

Even though price is a regular variable in main, the reflect.Value returned by reflect.ValueOf(price) is not settable. That's because Go passes price by value into reflect.ValueOf. The reflect.Value wraps a copy, not the original. Setting that copy would have no effect on price, so the runtime refuses to do it and panics instead of silently losing the write.

To make the value settable, you have to pass a pointer and then dereference it inside the reflect API.

Now we passed &price, which is a *float64. reflect.ValueOf(&price) wraps the pointer. .Elem() follows the pointer to the underlying float64. That underlying value lives at a known address in memory, so the runtime can mutate it through reflection. CanSet() reports whether a given Value is settable, and SetFloat does the write.

This rule generalizes: to modify anything through reflection, you must reach it via a pointer that the reflect package can follow. Read-only inspection works on copies; mutation needs an address.

The diagram shows the two patterns side by side. Passing x to reflect.ValueOf wraps a copy, which is fine for reading but not for writing. Passing &x and calling .Elem() wraps the original value at its real address, which the reflect package can then mutate.

TypeOf and ValueOf: A First Look

We've used both functions already, but it's worth pausing on them as the two foundational entry points. Almost every reflective program in Go begins with one of these calls.

reflect.TypeOf(v) returns a reflect.Type interface value. The methods on Type answer questions about the type itself, independent of any specific value. You ask a Type things like "what kind are you?", "do you have a name?", "are you a struct, and if so how many fields?".

reflect.ValueOf(v) returns a reflect.Value struct. A Value wraps both the type and the underlying data, so its methods let you read the data through typed accessors (.Int(), .String(), .Float()) and, if the value is settable, write to it.

The following uses both on a struct to show what's possible.

t.Name() returns the declared name of the type, Product. t.Kind() returns the broad category, struct. t.NumField() and t.Field(0) step through the struct's fields; v.Field(0) gives you the matching value. With TypeOf and ValueOf in hand, you have everything you need to start exploring any value.

A small but important detail: Type.Kind() is not the same as Type.Name(). A type's name is what you wrote in the type declaration (Product, Order, Customer). A type's kind is which of the built-in categories it falls into (struct, int, slice, map, and so on). Two different types can share a kind:

Email and Username are different named types, but both have kind string because they're both defined as string. When you're writing reflective code, you'll often switch on Kind() to decide how to handle a value, because the kind tells you what accessor methods are valid on the corresponding Value.

When You Actually Need Reflection

Reflection is a power tool. Use it deliberately. Most Go code doesn't need it, and reflective code is harder to read, slower at runtime, and untouched by the compiler's type checks. Before using reflection, ask whether one of the simpler alternatives would do the job.

NeedUse this firstReflection only if
One function for many types with shared behaviorAn interfaceThe behavior depends on field tags or layout
A function for any type with the same operationGenerics (Go 1.18+)The operation differs per concrete type at runtime
Encoding or decoding from JSON, XML, YAMLThe existing encoderYou're writing a new encoder for a new format
Comparing values for equality== or bytes.EqualValues are deeply nested with no common shape
Reading config from a file into a structjson.Unmarshal, yaml.UnmarshalYou're writing the unmarshaller

The first row is the most important. An interface says "I'll accept anything that has these methods", and the compiler checks the methods at the call site. Use an interface when the operations you need are the same regardless of the underlying type. A function that accepts an io.Reader doesn't care whether it's reading from a file, a network connection, or a byte buffer, because every implementation has the same Read method.

The second row is newer. Generics, added in Go 1.18, cover the case where you want one function to work for many types but with the same operation per type, like a Max function that takes two values of any ordered type. Before generics, this kind of code often used reflection. Now it doesn't have to.

Reflection is justified when the behavior depends on something only knowable at runtime. The classic example is JSON encoding: the encoder reads each field's tag (json:"name" or json:"-") to decide how to emit it. Tags are a runtime feature, attached to struct fields and readable through reflect.StructField.Tag. No interface and no generic type parameter can express "look at the tag of this field". So reflection fits there.

Another genuine use case is libraries that need to write into pointer destinations whose types they don't know. database/sql's rows.Scan(&customer.Name, &customer.Email) takes pointers of any type and fills them in from the SQL row. Inside, it has to figure out what kind of pointer it received and convert the database column to that type. There's no way to write that signature with interfaces alone, because the destination types are different on every call.

A pragmatic guideline: if your reflective code is a few lines of inspection inside a library, reflection is appropriate. If it's spreading through your application code, you've usually gone past where it should stop. Application code should consume libraries that use reflection, not write its own reflective logic.