Module 15: Reflection and Metaprogramming¶
← Module 14: Databases and Persistence | Topic Home | Module 16: Performance and Profiling →
Table of Contents¶
- Overview
- Learning Goals
- Prerequisites
- Why This Matters
- Historical Context
- Core Concepts
- The Three Laws of Reflection
- reflect.Type and reflect.Value
- Kind vs Type
- Inspecting Structs and Reading Struct Tags
- Addressability and Settability
- Constructing Values and Calling Methods
- Code Generation with go generate
- How the Concepts Fit Together
- Common Beginner Mistakes
- Mental Models
- Practical Examples
- Related Concepts
- Exercises
- Test
- Projects
- Further Reading
- Learning Journal
Overview¶
This module covers Go's reflect package, struct tags, and compile-time code generation with go generate. Reflection is the ability of a program to inspect and manipulate its own structure at runtime — to ask "what type is this value?", "what fields does this struct have?", "what methods does this object expose?" — without knowing those answers at compile time.
By the end of this module, you will understand how encoding/json, database/sql mappers, and ORMs work under the hood; how to read struct tags to build your own serializers; the precise rules governing which values can be set via reflection; and when code generation is the better alternative to runtime reflection. This knowledge sits at the boundary of meta-level programming in Go — essential for building frameworks, writing generic serialization, and understanding the standard library's most powerful packages.
Difficulty: Advanced | Estimated time: 5–7 hours
Learning Goals¶
By completing this module, you will be able to:
- State the three Laws of Reflection and explain what each one means — given a
reflect.Value, predict whether it is addressable and settable - Use
reflect.TypeOfandreflect.ValueOfto inspect types, kinds, and field metadata at runtime — write a function that prints every exported field name and value of any struct - Read struct field tags (e.g.,
json:"name,omitempty") using thereflect.StructTagAPI — implement a minimal struct-to-map serializer that respectsjsontags - Set values via reflection correctly, explaining why a pointer's
Elem()is required — write a function that takesinterface{}and zeroes all exportedintfields of the passed struct pointer - Explain the real costs of reflection — speed, loss of compile-time type safety, reduced readability — and articulate when generics or interfaces are the better choice — given a code review scenario, argue for or against using reflection
- Use
go generateto invoke a code generator, and explain why compile-time generation is preferable to runtime reflection for hot paths — add a//go:generatedirective and run it to produce aString()method withstringer
Prerequisites¶
Required Modules¶
- [[go/6. Methods and Interfaces]] — you need to understand: interface types, type assertions (
v.(T)and the comma-ok form), and the empty interfaceinterface{}/any; reflection operates on interface values and many reflect functions acceptany - [[go/4. Composite Types]] — you need to understand: struct declarations, field names and types, anonymous fields, and embedding; the reflect package exposes struct fields as
reflect.StructFieldvalues, so knowing struct layout is essential - [[go/10. Generics]] — you need to understand: what type parameters solve and their compile-time nature; the comparison with reflection (runtime introspection) becomes clearer when you have used generics and can articulate what each approach gives up
Required Concepts¶
- Interfaces and dynamic dispatch —
reflect.ValueOf(x)takes aninterface{}argument; understanding that the interface value carries both a type pointer and a data pointer is prerequisite to understanding the three Laws - Pointers and addressability — reflection's settability rules are directly tied to Go's pointer semantics; you cannot set a field in a value-type copy, only in a pointer-based value
- Struct field tags — the raw syntax (
`json:"name,omitempty"`) should be familiar from [[go/4. Composite Types]]; this module explains how to read them programmatically
[!TIP] If struct embedding or type assertions feel shaky, spend 20–30 minutes reviewing them before continuing. The settability rules in particular will be confusing without solid pointer semantics.
Why This Matters¶
Reflection is the machinery behind Go's most powerful standard-library packages. Without understanding it, you are using encoding/json, database/sql, and text/template as black boxes — you cannot debug unexpected behavior, extend them, or build analogous tools yourself.
Concretely, mastery of this module enables you to:
- Understand how
encoding/jsonworks —json.Marshalandjson.Unmarshalusereflectto inspect struct fields, readjsonstruct tags, and set field values dynamically; knowing this lets you debug marshaling surprises (why did this field get omitted? why is this field named differently in JSON?) without guessing - Build custom serializers and mappers — whether you are writing a configuration loader, an ORM field mapper, or a template renderer, the pattern is the same: iterate struct fields, read their tags, map to target representations; this module teaches that pattern directly
- Make informed tool choices — reflection has real costs (3–10× slower than direct calls, no compile-time type checking, panics on misuse); knowing when to use generics or interfaces instead — and when reflection is genuinely the right tool — is a senior-level skill
Without understanding reflection, you would be unable to read framework source code, debug mysterious serialization behavior, or build tooling that works generically across user-defined types — capabilities that appear constantly in advanced Go development.
Historical Context¶
Reflection as a language feature dates to Lisp in the 1970s and was formalized in languages like Smalltalk and Java. Java's java.lang.reflect package (introduced in Java 1.1, 1997) brought reflection to mainstream compiled languages and demonstrated both its power and its pitfalls: Spring Framework, Hibernate, and Jackson all rely on it heavily, but reflection-heavy Java code is notoriously slow and brittle.
Go's designers — Robert Griesemer, Rob Pike, and Ken Thompson — were aware of Java's experience. They chose to include reflection in the standard library (reflect, introduced in Go's first public release, 2009) but designed the language so that reflection is not required for ordinary programming. The interface system and, later, generics (Go 1.18, 2022) provide most of what Java uses reflection for, without the runtime cost.
Key moments in Go's reflection story:
- 2009 —
reflectpackage ships with Go's initial open-source release; the three Laws of Reflection are implicit in the design - 2011 — Rob Pike publishes "The Laws of Reflection" on the Go Blog, formalizing the mental model that remains canonical today
- 2011 —
go generateconcept begins forming; the community developsstringerand similar tools as reflection alternatives - 2014 —
go generate(go generatecommand) added in Go 1.4, formalizing compile-time code generation as a first-class workflow - 2022 — Go 1.18 generics give developers a type-safe, compile-time alternative to many reflection use cases; the question "generics or reflection?" becomes a real design choice
Understanding this history helps because it explains the "why not" alongside the "how": Go's designers gave you reflection as a last resort and an implementation tool for frameworks, not as the everyday abstraction mechanism it became in Java.
Core Concepts¶
The Three Laws of Reflection¶
Rob Pike's "Laws of Reflection" (Go Blog, 2011) are the foundational mental model. They are not arbitrary rules — they are consequences of how interface values work in Go.
Law 1: Reflection goes from interface value to reflection object.
reflect.TypeOf(x) and reflect.ValueOf(x) each take an interface{} and return a reflect.Type or reflect.Value representing the concrete value stored in that interface.
package main
import (
"fmt"
"reflect"
)
func main() {
x := 3.14
t := reflect.TypeOf(x) // reflect.Type describing float64
v := reflect.ValueOf(x) // reflect.Value holding 3.14
fmt.Println(t) // float64
fmt.Println(v) // 3.14
fmt.Println(t.Kind()) // float64 (the Kind)
fmt.Println(v.Float()) // 3.14 (extract the float64 value)
}
When you pass x to reflect.ValueOf, Go wraps it in an interface value. The reflect package then inspects the interface's internal type pointer and data pointer. This means everything reflection does starts from an interface value — that is Law 1.
Law 2: Reflection goes from reflection object back to interface value.
You can convert a reflect.Value back to an interface{} using its Interface() method, then use a type assertion to recover the concrete value.
v := reflect.ValueOf(3.14)
iface := v.Interface() // interface{} holding float64(3.14)
f := iface.(float64) // type assertion back to float64
fmt.Println(f) // 3.14
Law 3: To modify a reflection object, the value must be settable.
Not every reflect.Value can be modified. Setting requires the value to be addressable — which requires passing a pointer and calling Elem(). See Addressability and Settability for the full treatment.
reflect.Type and reflect.Value¶
reflect.Type describes the type of a value. reflect.Value holds a value (and allows inspection and, conditionally, modification).
package main
import (
"fmt"
"reflect"
)
type Point struct {
X, Y float64
}
func main() {
p := Point{X: 1.0, Y: 2.0}
t := reflect.TypeOf(p)
v := reflect.ValueOf(p)
fmt.Println("Type name:", t.Name()) // Point
fmt.Println("Type kind:", t.Kind()) // struct
fmt.Println("Num fields:", t.NumField()) // 2
// Iterate fields via Type
for i := 0; i < t.NumField(); i++ {
field := t.Field(i) // reflect.StructField
value := v.Field(i) // reflect.Value of the field
fmt.Printf(" Field %s (%s) = %v\n", field.Name, field.Type, value)
}
// Output:
// Field X (float64) = 1
// Field Y (float64) = 2
}
Key methods on reflect.Type:
- Name() — type name (empty for unnamed types like []int)
- Kind() — the underlying kind (see next section)
- NumField(), Field(i) — struct fields
- NumMethod(), Method(i) — methods in the method set
Key methods on reflect.Value:
- Kind() — same Kind as the type
- Interface() — returns the value as interface{}
- Field(i), Index(i), MapIndex(key) — access composites
- Call(args) — call a function or method value
- Set(v), SetInt(n), SetString(s) — modify (requires settability)
Kind vs Type¶
Kind and Type are frequently confused. The distinction is important:
reflect.Typeis the full, specific type —int32,Point,[]string,map[string]intare all distinctreflect.Typevalues.reflect.Kindis the underlying structural category — there are ~26 kinds:Bool,Int,Int8,Int16,Int32,Int64,Uint, ...,Float32,Float64,Complex64,Complex128,Array,Chan,Func,Interface,Map,Pointer,Slice,String,Struct,UnsafePointer.
type Celsius float64
type Fahrenheit float64
c := Celsius(100)
f := Fahrenheit(212)
tc := reflect.TypeOf(c)
tf := reflect.TypeOf(f)
fmt.Println(tc.Name(), tc.Kind()) // Celsius float64
fmt.Println(tf.Name(), tf.Kind()) // Fahrenheit float64
// Same Kind, different Type
fmt.Println(tc == tf) // false — different types
fmt.Println(tc.Kind() == tf.Kind()) // true — same kind
When writing code that handles arbitrary values (a serializer, an ORM mapper), you almost always switch on Kind, not Type, because Kind tells you how to extract the underlying data. Two user-defined types with the same Kind can be handled by the same code path.
// Generic int-extractor: works for any int-kinded type
func toInt64(v reflect.Value) (int64, bool) {
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int(), true
default:
return 0, false
}
}
Inspecting Structs and Reading Struct Tags¶
Struct tags are the mechanism by which packages like encoding/json and database/sql allow users to customize field mapping without writing boilerplate. A tag is a raw string literal attached to a struct field:
type User struct {
ID int `json:"id" db:"user_id"`
Name string `json:"name,omitempty" db:"full_name"`
Password string `json:"-"` // omit from JSON entirely
internal int // unexported — invisible to reflection callers
}
To read tags at runtime, use reflect.StructField.Tag — a value of type reflect.StructTag — and call its Get(key) or Lookup(key) method:
package main
import (
"fmt"
"reflect"
)
type User struct {
ID int `json:"id" db:"user_id"`
Name string `json:"name,omitempty" db:"full_name"`
}
func printTags(v interface{}) {
t := reflect.TypeOf(v)
if t.Kind() == reflect.Pointer {
t = t.Elem() // dereference pointer to get struct type
}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
jsonTag := f.Tag.Get("json") // "" if absent
dbTag := f.Tag.Get("db")
fmt.Printf("Field: %-8s json:%q db:%q\n", f.Name, jsonTag, dbTag)
}
}
func main() {
printTags(User{})
// Output:
// Field: ID json:"id" db:"user_id"
// Field: Name json:"name,omitempty" db:"full_name"
}
Tag.Get(key) returns the empty string if the key is absent. Tag.Lookup(key) returns (string, bool) — the bool is false if the key is genuinely absent (as opposed to present with an empty value). Prefer Lookup when the distinction matters.
[!NOTE] How
encoding/jsonuses this: Whenjson.Marshalencounters a struct, it iterates fields viareflect.Type.NumField/Field(i), reads thejsonstruct tag, and uses the tag value as the JSON key. When the tag containsomitempty, it skips zero-valued fields. When the tag is"-", it skips the field entirely. The full parsing of multi-option tags (splitting on,) is a few lines of string manipulation layered on top ofTag.Get("json").
Addressability and Settability¶
This is where most reflection bugs originate. The rules:
- A
reflect.Valueobtained byreflect.ValueOf(x)wherexis a non-pointer is not addressable and not settable — modifying it would modify a copy. - To get a settable value, pass a pointer:
reflect.ValueOf(&x), then call.Elem()to get the value the pointer points to. That value is both addressable and settable. - Even with an addressable struct, only exported fields are settable. Calling
Seton an unexported field panics.
package main
import (
"fmt"
"reflect"
)
type Config struct {
MaxRetries int
Host string
timeout int // unexported — cannot be set via reflection
}
func zeroInts(ptr interface{}) {
v := reflect.ValueOf(ptr)
if v.Kind() != reflect.Pointer || v.Elem().Kind() != reflect.Struct {
panic("zeroInts requires a pointer to a struct")
}
s := v.Elem() // the struct value — addressable
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
// CanSet returns false for unexported fields
if f.Kind() == reflect.Int && f.CanSet() {
f.SetInt(0)
}
}
}
func main() {
cfg := Config{MaxRetries: 5, Host: "localhost", timeout: 30}
fmt.Println("Before:", cfg) // {5 localhost 30}
zeroInts(&cfg)
fmt.Println("After:", cfg) // {0 localhost 30}
// timeout is unchanged — it's unexported, CanSet() returned false
}
Why the pointer dance is necessary: When you pass a value to a function (including reflect.ValueOf), Go copies it. The reflect.Value holds a copy — not the original. Setting the copy would have no effect on the caller's variable. Passing a pointer means reflect.ValueOf receives the pointer value, Elem() follows it to the original data, and Set writes through the pointer to the original.
Checking before setting:
// Always check CanSet before calling Set — a panic is worse than a no-op
if f.CanSet() {
f.Set(newValue)
}
Constructing Values and Calling Methods¶
Reflection can create new values and call methods dynamically — the basis for dependency injection frameworks and RPC systems.
package main
import (
"fmt"
"reflect"
)
// Adder adds two ints
type Adder struct{ Base int }
func (a Adder) Add(x int) int { return a.Base + x }
func main() {
// Construct a new Adder using reflection
t := reflect.TypeOf(Adder{})
newVal := reflect.New(t) // *Adder (pointer)
newVal.Elem().FieldByName("Base").SetInt(10)
// Call the Add method via reflection
method := newVal.Elem().MethodByName("Add")
args := []reflect.Value{reflect.ValueOf(5)}
results := method.Call(args)
fmt.Println(results[0].Int()) // 15
}
reflect.New(t) allocates a new zero value of type t and returns a reflect.Value of kind Pointer. MethodByName looks up a method by name in the value's method set; Call invokes it with a slice of reflect.Value arguments and returns a slice of reflect.Value results.
[!WARNING]
Callpanics if the number or types of arguments do not match the method signature. There is no compile-time check. This is one of reflection's main drawbacks — errors surface at runtime, not build time.
Code Generation with go generate¶
go generate is the compile-time alternative to runtime reflection. Instead of inspecting types at runtime, you write a program (or use an existing tool) that reads your Go source, generates new .go files, and those generated files are compiled as normal Go code — with full type safety, zero reflection overhead, and IDE support.
The workflow:
- Add a
//go:generatedirective comment to any.gofile:
// In types.go
//go:generate stringer -type=Direction
package main
// Direction is a compass direction.
type Direction int
const (
North Direction = iota
East
South
West
)
-
Run
go generate ./...— Go finds every//go:generatedirective in your source and executes the specified command. -
The
stringertool readstypes.go, finds theDirectiontype and its constants, and writesdirection_string.gocontaining aString() stringmethod that returns"North","East", etc. — without any runtime reflection.
Why go generate over runtime reflection for String() methods:
// Generated code — zero overhead, type-checked at compile time:
func (d Direction) String() string {
switch d {
case North:
return "North"
case East:
return "East"
case South:
return "South"
case West:
return "West"
default:
return fmt.Sprintf("Direction(%d)", int(d))
}
}
A reflection-based String() that uses reflect.TypeOf(d).Name() and iterates constants would work but runs reflection machinery on every call. The generated version is a single switch — nanoseconds vs. microseconds on hot paths. See [[go/16. Performance and Profiling]] for how to measure this difference with benchmarks.
Other common go generate tools:
- stringer — String() methods for enum-like constants
- mockgen (gomock) — interface mock implementations
- protoc-gen-go — Protocol Buffer marshaling code from .proto files
- sqlc — type-safe SQL query code from .sql files
[!NOTE] Generated files should be committed to version control. Name them with a
_gen.goor_string.gosuffix by convention. Add a comment at the top:// Code generated by ...; DO NOT EDIT.
How the Concepts Fit Together¶
Any Go value
│
▼
interface{} / any ──────────────────── Law 1: ValueOf / TypeOf
│
├──► reflect.Type ───────────── Name, Kind, NumField, Field(i).Tag
│
└──► reflect.Value ───────────── Kind, Interface(), Field(i)
│
├── not addressable ── read-only (passed by value)
│
└── addressable ─────── pass pointer + Elem()
│
└── CanSet() true → Set / SetInt / SetString
│
▼
Law 3: modification through reflect
Struct tags: reflect.StructField.Tag.Get("json") / .Lookup("json")
│
└──► encoding/json, database/sql mappers, ORMs — all use this
Runtime reflection cost is real: 3–10× slower than direct calls
│
└──► compile-time alternative: go generate + stringer / mockgen / sqlc
The central insight is that reflection is the runtime counterpart to compile-time type information. Everything the compiler knows statically, you can ask at runtime via reflect — but at the cost of type safety and performance. Generics ([[go/10. Generics]]) and interfaces ([[go/6. Methods and Interfaces]]) are the compile-time tools that make reflection unnecessary for most cases.
Common Beginner Mistakes¶
[!WARNING] Mistake 1: Trying to set a value obtained from a non-pointer
Calling
SetorSetInton areflect.Valueobtained from a plain value (not a pointer) panics with "reflect: reflect.Value.SetInt using value obtained using unexported field" or "reflect.Value.Set using unaddressable value".Wrong:
x := 42 v := reflect.ValueOf(x) v.SetInt(100) // PANIC: reflect.Value.SetInt using unaddressable valueRight:
Why this matters: The panic only appears at runtime, not at compile time — you can ship this bug to production.
[!WARNING] Mistake 2: Calling Set on an unexported field
Even with a properly addressable value,
Seton an unexported field panics: "reflect.Value.SetInt using value obtained using unexported field". Always callCanSet()beforeSet.Wrong:
type T struct{ x int } // unexported val := T{x: 5} reflect.ValueOf(&val).Elem().FieldByName("x").SetInt(10) // PANICRight:
f := reflect.ValueOf(&val).Elem().FieldByName("x") if f.CanSet() { f.SetInt(10) } // silently skips unexported field — no panic[!WARNING] Mistake 3: Switching on Type instead of Kind for generic handlers
If you write a generic handler that switches on
reflect.Type(using==comparisons), it will not match user-defined types with the same underlying type.Wrong:
Right:
Other pitfalls:
- Forgetting to handle pointer-to-struct — many functions receive
interface{}that may be eitherTor*T; always checkv.Kind() == reflect.Pointerand callv.Elem()to normalize - Reflection across package boundaries for unexported fields —
reflectrespects Go's export rules; unexported fields are not accessible from outside the package; this is intentional and correct - Overusing reflection for things generics solve — since Go 1.18, writing
func Map[T any](s []T, f func(T) T) []Tis better than a reflection-based mapper in almost every case; see [[go/10. Generics]]
Mental Models¶
The right mental model makes this module click. Here are the most useful ways to think about it:
Mental Model 1: The X-ray Machine¶
Think of reflect.ValueOf as an X-ray machine. You hand it any opaque box (an interface{}), and it produces an image (a reflect.Value) that reveals the internal structure — the fields, their types, their current values. The X-ray image is not the original box; it is a picture of it. Reading from the picture is fine. Modifying the original requires going back through the original pointer, not editing the picture.
This model explains addressability intuitively: an X-ray of a copy cannot change the original. To change the original, you need to hand the machine the original itself (a pointer), get an X-ray, and then write back through the pointer.
This model breaks down when thinking about Call — calling a method via reflection does execute on the real value, not a copy.
Mental Model 2: Struct Tags as Metadata Annotations¶
Think of struct tags as a lightweight annotation system, similar to Java annotations or Python decorators, but evaluated at runtime via reflect.StructField.Tag.Get(). The struct is the schema; the tags are its documentation for downstream consumers.
Every package that reads struct tags is effectively implementing a small language. encoding/json implements JSON field naming, omitempty, and skip. database/sql mappers implement column aliasing. Your own code can implement whatever mapping rules you need. The reflect.StructTag.Get(key) API is the universal accessor for all of them.
This is most useful when you are debugging unexpected JSON output: find the struct, find the tag, check whether it says omitempty or "-". The tag is the contract between your struct definition and the marshaler.
Mental Model 3: Reflection as the Runtime Compiler¶
The Go compiler processes your source code at build time and generates type-checked machine code. Reflection is what you use when you need that same type information at runtime — when the types are not known when you write the code. A JSON marshaler cannot know your specific struct layout at compile time; it must discover it at runtime.
But runtime always costs more than compile time. Every reflection call that the Go compiler could have resolved statically is now resolved dynamically. This is why go generate + code generation tools exist: they run a compiler-like analysis at build time and produce static Go code, giving you the flexibility of reflection with the speed of compiled code.
Use this model when deciding reflection vs. generics vs. go generate: if the types are constrained and expressible as type parameters, use generics; if the types are truly arbitrary and only known at runtime, use reflection; if the types are known but numerous and writing the code manually is tedious, use go generate.
[!NOTE] No single mental model is perfect. Use Model 1 (X-ray machine) when reasoning about addressability and why you need
Elem(). Use Model 2 (metadata annotations) when working with struct tags and serializers. Use Model 3 (runtime compiler) when choosing between reflection, generics, and code generation.
Practical Examples¶
Example 1: Printing Any Struct's Fields (Basic)¶
Scenario: A debugging utility that prints every exported field of any struct, regardless of its type.
Goal: Demonstrate reflect.TypeOf, reflect.ValueOf, struct field iteration, and the Kind check.
package main
import (
"fmt"
"reflect"
)
// PrintFields prints every exported field name and value of any struct.
func PrintFields(v interface{}) {
rv := reflect.ValueOf(v)
rt := reflect.TypeOf(v)
// Dereference pointer if needed
if rv.Kind() == reflect.Pointer {
rv = rv.Elem()
rt = rt.Elem()
}
if rv.Kind() != reflect.Struct {
fmt.Println("(not a struct)")
return
}
for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
value := rv.Field(i)
// PkgPath is empty for exported fields
if !field.IsExported() {
continue
}
fmt.Printf(" %-12s (%s) = %v\n", field.Name, field.Type, value)
}
}
type Server struct {
Host string
Port int
TLS bool
secret string // unexported — skipped
}
func main() {
s := Server{Host: "localhost", Port: 8080, TLS: true, secret: "abc"}
fmt.Println("Server fields:")
PrintFields(s)
// Output:
// Server fields:
// Host (string) = localhost
// Port (int) = 8080
// TLS (bool) = true
}
What to notice: field.IsExported() (Go 1.17+) replaces the older field.PkgPath == "" idiom. The function handles both T and *T inputs by normalizing the pointer. Unexported fields (secret) are silently skipped — they are visible to NumField but not accessible for reading via Interface().
Example 2: A Struct-to-Map Serializer with Tag Support (Intermediate)¶
Scenario: A minimal serializer that converts any struct to map[string]interface{}, respecting json struct tags for key names, and honoring the omitempty option.
Goal: Demonstrate struct tag reading, Tag.Get, option parsing, and zero-value detection — the same core logic encoding/json uses internally.
package main
import (
"fmt"
"reflect"
"strings"
)
// StructToMap converts a struct to map[string]interface{},
// using the `json` struct tag for key names.
// Fields tagged `json:"-"` are omitted.
// Fields tagged with `omitempty` are omitted if zero.
func StructToMap(v interface{}) map[string]interface{} {
rv := reflect.ValueOf(v)
rt := reflect.TypeOf(v)
if rv.Kind() == reflect.Pointer {
rv, rt = rv.Elem(), rt.Elem()
}
if rv.Kind() != reflect.Struct {
return nil
}
result := make(map[string]interface{})
for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
val := rv.Field(i)
if !field.IsExported() {
continue
}
// Parse the json tag
tag := field.Tag.Get("json")
if tag == "-" {
continue // explicitly excluded
}
name := field.Name // default key is field name
omitempty := false
if tag != "" {
parts := strings.SplitN(tag, ",", 2)
if parts[0] != "" {
name = parts[0]
}
if len(parts) > 1 && parts[1] == "omitempty" {
omitempty = true
}
}
// omitempty: skip zero values
if omitempty && val.IsZero() {
continue
}
result[name] = val.Interface()
}
return result
}
type Article struct {
ID int `json:"id"`
Title string `json:"title"`
Draft bool `json:"draft,omitempty"` // omitted when false
Secret string `json:"-"` // always omitted
Author string // no tag — uses field name
}
func main() {
a := Article{ID: 1, Title: "Hello", Author: "Alice", Secret: "x"}
m := StructToMap(a)
for k, v := range m {
fmt.Printf(" %q: %v\n", k, v)
}
// Output (map order varies):
// "id": 1
// "title": Hello
// "Author": Alice
// (draft omitted — zero bool; Secret omitted — tagged "-")
}
What to notice: strings.SplitN(tag, ",", 2) is the minimal tag parser. val.IsZero() (Go 1.13+) checks whether the value equals its type's zero value — the same logic encoding/json uses for omitempty. The map key defaults to the field name when no tag is present, matching encoding/json behavior exactly.
Example 3: Using go generate with stringer (Applied)¶
Scenario: An application defines a Status type for HTTP-like response codes. Without go generate, printing a Status value gives a useless integer. With stringer, it gives a readable name.
Goal: Show the //go:generate directive, the workflow, and the result — demonstrating code generation as a compile-time alternative to reflection.
// file: status.go
//go:generate stringer -type=Status -output=status_string.go
package main
import "fmt"
// Status represents an operation result code.
type Status int
const (
StatusOK Status = iota // 0
StatusPending // 1
StatusFailed // 2
StatusTimeout // 3
)
func main() {
s := StatusPending
fmt.Println(s) // Before generation: prints "1"
// After generation: prints "StatusPending"
fmt.Println(s == StatusPending) // true
}
After running go generate ./... (which requires stringer to be installed: go install golang.org/x/tools/cmd/stringer@latest), the tool produces:
// file: status_string.go — Code generated by stringer; DO NOT EDIT.
package main
import "strconv"
func _() {
// Compile-time check: this assignment fails if constants are changed
var x [1]struct{}
_ = x[StatusOK-0]
_ = x[StatusPending-1]
_ = x[StatusFailed-2]
_ = x[StatusTimeout-3]
}
const _Status_name = "StatusOKStatusPendingStatusFailedStatusTimeout"
var _Status_index = [...]uint8{0, 8, 21, 33, 46}
func (i Status) String() string {
if i < 0 || i >= Status(len(_Status_index)-1) {
return "Status(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Status_name[_Status_index[i]:_Status_index[i+1]]
}
What to notice: The generated String() method uses a precomputed string and index table — O(1) lookup, zero allocations, no reflection. The compile-time check (var x [1]struct{}; _ = x[StatusOK-0]) ensures the file is regenerated if the constants change. This is the pattern you want on hot paths where a reflection-based stringer would be measurably slower.
Example 4: The Reflection Cost Reality Check (Edge Case)¶
Scenario: Comparing the performance of a direct field access, a reflection-based field access, and a generated accessor — showing concretely why reflection should be used sparingly on hot paths.
package main
import (
"fmt"
"reflect"
"time"
)
type Point struct{ X, Y float64 }
func directAccess(p *Point) float64 { return p.X }
func reflectAccess(p *Point) float64 {
return reflect.ValueOf(p).Elem().FieldByName("X").Float()
}
func main() {
p := &Point{X: 3.14, Y: 2.71}
N := 1_000_000
// Direct access
start := time.Now()
for i := 0; i < N; i++ {
_ = directAccess(p)
}
fmt.Printf("Direct: %v for %d iterations\n", time.Since(start), N)
// Reflection-based access
start = time.Now()
for i := 0; i < N; i++ {
_ = reflectAccess(p)
}
fmt.Printf("Reflect: %v for %d iterations\n", time.Since(start), N)
// Typical results on modern hardware:
// Direct: ~2ms (≈ 2ns/op)
// Reflect: ~30ms (≈ 30ns/op) — 10–15x slower
}
Why this is important: The 10–15× slowdown is real and consistent. For one-off operations (marshaling a struct once per HTTP request), it is rarely a bottleneck. For hot-path operations (processing millions of records per second, inner loops), it matters. The rule of thumb: use reflection freely in initialization code and framework internals; avoid it in per-call hot paths. go generate and generics are the escape hatches. See [[go/16. Performance and Profiling]] for how to measure this precisely with go test -bench.
Related Concepts¶
Within this topic:
- [[go/6. Methods and Interfaces]] — reflection operates on interface values; type assertions and type switches are the static equivalents of
reflect.TypeOfand kind-switching; choosing between a type switch and reflection is a recurring design decision - [[go/4. Composite Types]] — struct declarations with field tags are the primary target of reflection in practice; understanding struct embedding and anonymous fields matters when iterating fields with
NumField/Field(i)(embedded fields appear as promoted fields) - [[go/10. Generics]] — type parameters are the compile-time alternative to reflection for type-parameterized code; when you find yourself using
reflectto handle "any type that supports X", ask whether a type constraint would work instead - [[go/16. Performance and Profiling]] — quantifying the actual overhead of your reflection code with
pprofand benchmarks; the Performance module explains the tools for measuring whether reflection is actually your bottleneck
Exercises¶
Practice problems are in EXERCISES.md.
Preview — Exercise 1:
Read Type and Kind (Easy)
Write a function
typeInfo(v interface{})that prints thereflect.Typename andreflect.Kindof any value passed to it. Call it with at least five values of different kinds: anint, astring, astruct, a slice, and a map.
The exercises progress from basic type inspection through struct tag reading, a struct-to-map serializer, and a go:generate task. Complete at least the Easy and Medium exercises before taking the test.
Test¶
When you feel ready, take the self-assessment: TEST.md
Test overview: - Section 1: Recall (5 questions, 1 pt each) - Section 2: Conceptual Understanding (3 questions, 2 pts each) - Section 3: Applied / Practical (2 questions, 3 pts each) - Section 4: Scenario / Debugging (1 question, 3 pts) - Section 5: Discussion (1 question, 2 pts) - Section 6: Bonus Challenge (1 question, 5 pts bonus)
Passing: ≥ 70% of non-bonus points (≥ 15/22). Aim for ≥ 80% (≥ 18/22).
Projects¶
See the topic-level PROJECTS.md for project ideas.
Recommended project after this module:
Config Loader — write a function LoadConfig(path string, cfg interface{}) that reads a YAML or JSON file and populates a struct passed by pointer, using reflection to map file keys to struct fields via json or yaml struct tags. This requires struct tag reading, settability, type coercion across kinds, and robust error handling for mismatched types — a real-world reflection task.
Further Reading¶
These are verified, high-quality resources specifically relevant to this module:
- The Laws of Reflection — The Go Blog — Rob Pike's canonical post (2011) explaining the three laws with running examples; still the single best introduction; read before anything else in this module
- pkg.go.dev/reflect — The complete
reflectpackage API reference; the Overview section is worth reading in full; use the function list as a reference when writing reflection code - Generating code — The Go Blog — Rob Pike's post on
go generate(2014): motivation, the//go:generatedirective, and how tools likestringerintegrate; essential reading before thego generateexercise - "The Go Programming Language" — Donovan & Kernighan, Chapter 12 — The most thorough treatment of reflection in any Go book; covers the display, encoding, and format examples that demonstrate both the power and the danger of reflection in real programs
For a complete resource list, see the topic-level RESOURCES.md.
Learning Journal¶
Record your experience studying this module. Be specific — vague entries are useless later. Newest entries at the top.
YYYY-MM-DD — Started Module 15¶
What I covered today: - Read the Overview and Why This Matters sections - Worked through Core Concepts up to (concept name here)
What clicked: - Something that made sense
What's still unclear: - Something that's still fuzzy — add to QUESTIONS.md
Questions logged: - See QUESTIONS.md Q001
Test score: Not taken yet
Add new entries above this line.