Reflection vs Code Generation in Go
Go offers two ways to write generic-looking code without C++-style templates: inspect values at runtime with reflect, or generate typed code before the compiler runs.
Search across all documentation pages
Go offers two ways to write generic-looking code without C++-style templates: inspect values at runtime with reflect, or generate typed code before the compiler runs.
Neither approach is universally better.
Reflection keeps libraries flexible and user code short.
Codegen keeps CPU predictable and errors compile-time.
SME teams succeed when they know which tool owns which layer of the stack.
reflect.Type, reflect.Value, struct tags, go generate, AST/template generators, go:fix inline, build tags.go generate, custom generators, ORM reflection costs, API migration directives.Go deliberately omitted a macro system and user-defined compile-time templates.
Instead, the language gives you struct tags (string metadata on fields), a small reflect package, and tooling hooks (go generate, go fix) that run before go build.
Reflection answers questions like "what fields does this struct have?" or "is this value a pointer to an int?" at runtime.
The encoding/json package is the canonical example: json.Marshal(v) walks v with reflection unless v implements json.Marshaler.
Code generation answers the same questions once, ahead of time.
Tools read your types (often via go/ast or go/types) and write .go files with explicit field assignments, switch cases, or String() methods.
The stringer tool generates enum string methods; protoc-gen-go generates protobuf structs and getters.
A useful analogy: reflection is an interpreter that reads a schema on every request; codegen is compiling that schema into a dedicated function.
Both approaches usually start from the same source of truth: Go type definitions and struct tags.
type definitions + struct tags
|
+-----+-----+
| |
reflect go generate /
at runtime go fix / AST tools
| |
generic specialized
library .go files
logic compiled normally
At runtime, reflection walks reflect.Type (shape) and reflect.Value (data).
Libraries cache reflect.Type values in package-level maps so repeated work amortizes - but the first use and every field access still pays more than a direct field read.
Codegen inlines that walk into straight assignments or switch statements.
The compiler can dead-code eliminate, inline, and escape-analyze the result like hand-written code.
Struct tags sit in the middle.
Tags are compile-time constants attached to fields (json:"name,omitempty").
Parsers read them via reflect.StructField.Tag at runtime, or generators read them from AST and bake decisions into generated code.
go generate is the standard orchestration command.
A source file contains a directive such as //go:generate go run ./gen.go.
Running go generate ./... executes those commands; CI typically runs it before go test when generated files are committed.
go fix (Go 1.26+) adds modernizers including the inline analyzer driven by //go:fix inline directives - a form of migration codegen applied across call sites.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Reflection | Flexible, no build step, small source footprint | Slower hot paths, runtime panics on misuse | Frameworks, config, admin tools |
| Codegen | Fast, compile-time errors, explicit code to grep | Generator upkeep, noisy diffs, build ordering | Encoders, ORMs on hot paths, large enums |
| Hybrid (reflect once, cache) | Balance for medium traffic | Cache invalidation, memory | Middleware, validation at startup |
| Hand-written only | Simplest for few types | Does not scale with type count | Tiny APIs, stable schemas |
Production services often use reflection at the edges and codegen in the core.
An HTTP gateway might reflect request bodies into structs for occasional admin endpoints while protobuf/gRPC handlers use generated code from .proto files.
ORMs like GORM reflect struct tags into SQL at runtime; sqlc and ent generate type-safe query code instead.
For Kubernetes controllers, kubebuilder generates typed clients; reflection appears in generic serializers and scheme registration, not in tight reconcile loops.
Security: reflection on interface{} or map[string]any from untrusted input needs bounds checks.
Codegen that emits switch on known enums reduces the attack surface.
Observability: reflection-heavy handlers show up in CPU profiles under reflect.*.
Benchmark with testing.B and pprof before optimizing; often a targeted generator for one struct family is enough.
Version upgrades: when you bump Go, rerun go generate and go fix.
Generated files may change formatting; //go:fix inline migrations propagate API renames without manual sed.
TinyGo and WASM: reflection support is limited or costly on tiny targets.
Prefer codegen for embedded Go binaries.
encoding/json remains appropriate for many APIs; codegen supplements it where benchmarks demand it.go generate explicitly or use go:generate in Makefile/CI.Reflection inspects types while the program runs.
Codegen writes normal Go source before compile so the compiler sees fixed types and calls.
Yes, for default marshaling.
Types implementing json.Marshaler / json.Unmarshaler bypass reflection for that type.
Tags attach string metadata to fields.
Libraries read tags via reflection or generators to control naming, validation, and storage mapping.
No.
go generate runs external commands (often generators) that emit source files.
Reflection is a runtime library package.
Yes.
Common pattern: generate repetitive boilerplate, use reflection for optional plugin registration or one-time schema discovery at startup.
A directive on deprecated forwarding functions telling Go 1.26's inline modernizer to rewrite call sites to the new API automatically.
To map arbitrary structs to tables without per-type hand-written SQL.
The flexibility costs CPU; sqlc/ent trade flexibility for generated queries.
Benchmark with testing.B, profile with pprof, compare against a generated or hand-written baseline on the same payload sizes.
Support is limited compared to the main Go compiler.
Embedded and WASM projects should prefer codegen or hand-written mappings.
Invalid Set on unaddressable values, wrong Kind assumptions, and panics from Interface() on unexported fields across packages.
Type, Value, and field iterationStack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026