Reflection & Code Generation Best Practices
Choose reflection for frameworks, codegen for hot paths.
These rules turn the reflection and codegen articles in this section into reviewable habits for libraries, services, and platform teams.
How to Use This List
- Apply during API design, performance reviews, and dependency upgrades.
- Separate framework init (reflection OK) from per-request work (prefer codegen or hand code).
- Revisit when adding ORMs, JSON APIs, or monorepo-wide API migrations.
A - Choosing Reflection vs Codegen
- Profile before optimizing. Use
pprofand benchmarks to prove reflection is a bottleneck, not assumed. - Use reflection on cold paths (startup binding, CLI parsing, test helpers). Avoid it in inner RPC or DB loops without caching.
- Prefer codegen when types are known at build time and the same encoder or mapper runs thousands of times per second.
- Implement
Marshaler/Unmarshaleron proven hot types before adopting a new JSON generator framework. - Keep DTOs flat when serializers or ORMs reflect over many nested fields.
B - Struct Tags and Metadata
- One tag grammar per key documented in the package README (
validate,json,db, custom). - Use
Tag.Lookupwhen key presence matters; do not rely onGetalone to detect missing keys. - Run struct tag linters in CI to catch duplicate
jsonnames and invalid tag syntax. - Parse tags once per
reflect.Typeand cache field plans insync.Mapor package init. - Do not store secrets in struct tags; tags are compile-time visible metadata, not a vault.
C - go generate and Custom Generators
- Commit generated files with
// Code generated ... DO NOT EDITheaders unless all consumers always run generators. - Fail CI when
go generate ./...produces a diff so drift is caught before merge. - Pin generator tools in
tools.go(build tagtools) and install that version in CI. - Always
format.Sourcegenerator output sogofmtchecks pass. - Write golden tests for custom generators using small
testdata/inputs.
D - Performance and Operations
- Benchmark with
-benchmemwhen comparing reflective JSON/ORM paths to generated alternatives. - Watch for
reflect.*in CPU profiles under marshal, scan, and validation handlers. - Pool buffers for encoders on HTTP/gRPC edges even when using codegen.
- Regenerate after Go upgrades alongside
go fixandgo test ./.... - Document TinyGo/WASM limits where reflection is reduced; prefer codegen on embedded targets.
E - API Migrations with go fix
- Add
//go:fix inlineon forwarding shims when renaming or moving symbols, withDeprecated:godoc. - Preview with
go fix -diffon large repos before applying sweeping changes. - Run
go fixtwice on upgrade branches when release notes recommend synergistic passes. - Keep shim bodies simple (ideally one call to the new API) so the inliner produces tidy output.
- Test after batch fix; unused locals can appear when multiple fixes interact.
FAQs
Should we ban reflection entirely?
No.
Ban unbounded per-request reflection without measurement.
Framework initialization and admin tools are fine.
When is codegen mandatory?
When benchmarks and SLOs prove reflective serializers or ORM scans dominate CPU or allocations.
Not before evidence exists.
Who owns go generate in CI?
Platform or service owners add the step to the standard pipeline.
Same team reviews generator version bumps.
How do we review large generated diffs?
Separate commits: one for hand-written type changes, one for go generate output only.
Can we mix reflection and codegen?
Yes.
Common pattern: reflect once at startup, codegen for per-request hot types.
What about third-party ORMs?
Accept reflection cost for velocity on low-QPS paths.
Introduce sqlc/ent or slim DTOs when profiles demand it.
How do //go:fix inline and deprecations interact?
Deprecation tells humans to migrate.
Inline shims let go fix automate call sites when forwarding functions exist.
Should basics pages use reflection in examples?
Yes for teaching.
Production code in the same service should follow the rules above.
How do we onboard new hires?
Point them to Reflection Basics, then this list during first API or ORM review.
What linters help?
govet, errcheck, struct tag analyzers, and CI go generate diff checks.
Related
- Reflection vs Code Generation in Go - conceptual overview
- Reflection Basics - hands-on primitives
- go generate & stringer - standard codegen workflow
- go:fix inline & API Migration Directives - automated migrations
- ORM & Serializer Reflection Costs - when to optimize
Stack 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).