Skip to content
Dev Tools Article

Go Keeps Choosing Codegen Over Reflection

A new five-generator suite matters less than the workflow behind it, which Go 1.24 finally made first-class.

Lenn Voss
Lenn Voss
Cloud & Infrastructure Writer · Aug 20, 2026 · 5 min read
Go Keeps Choosing Codegen Over Reflection

A developer named Tetsuro Mikami recently shipped kanna, a suite of five Go code generators covering dependency-injection wiring, test fixtures, domain-to-API type mapping, ORM-style row scanning, and typed i18n messages. The project is at v0.0.1 with a handful of stars, so let's be clear up front: this is not a tool you bet production on this week. But it's worth your attention anyway, because it's a clean specimen of a pattern that keeps winning in Go — and because the Go toolchain quietly removed the last excuse not to do this yourself.

The pendulum keeps landing on codegen

Go has been fighting the same boilerplate war since before generics. Every category of mechanical code — constructors, mocks, mappers, scanners — eventually attracts two camps: libraries that erase the boilerplate with runtime reflection, and generators that emit the code you'd have written by hand.

The reflection camp has real wins. fx and dig power dependency injection across Uber's Go monorepo; GORM is still probably the most-installed way to talk to a database from Go. The pitch is seductive: no build step, no generated files cluttering your diffs.

But look at where the momentum actually is. sqlc — which compiles SQL into type-safe Go at build time — has become the default recommendation for new Go services in most threads where GORM's reflection surprises come up. mockgen outlived its own maintainer: Google archived golang/mock in 2023 and Uber forked it as go.uber.org/mock because teams refused to give up generated mocks. And wire, Google's compile-time DI generator, was archived in August 2025 — yet its core idea didn't die with it. Kanna's kanna-di is essentially wire's philosophy reborn with less ceremony: scan for provider functions, generate a constructor that wires the graph in dependency order, fail at compile time if something's missing.

That's the pattern worth naming. In Go, individual codegen tools die all the time. The codegen approach keeps getting re-implemented, because the failure mode of reflection — a panic at 2 a.m. inside a library you can't step through — is exactly the failure mode Go culture is allergic to. Generated code shows up in git diff, gets read in code review, and works in a debugger. Mikami's framing is the standard argument, competently executed: generate what you were going to write anyway, and commit it.

Generics were supposed to end this argument, and didn't. Type parameters abstract over types; boilerplate in Go is mostly derived from shape — the specific field list of a specific struct. No type parameter will write user.Email = req.Email for forty fields, or know that CreatedAt should be set on insert. That code is either reflected into existence at runtime or generated at build time. Those are still the only two options, and Go keeps choosing the second.

Go 1.24 changed the economics

Here's the part that makes 2026 different from the wire era. Until recently, depending on a codegen tool meant the tools.go hack — a file of blank imports whose only job was to pin tool versions — plus per-developer go install drift. Go 1.24 replaced that with a first-class tool directive in go.mod:

go get -tool github.com/go-kanna/kanna/cmd/kanna-di

That pins the generator's version in go.mod like any other dependency, and go tool kanna-di runs it from a //go:generate line. Every developer and every CI runner gets the exact same generator version with zero setup. Combine that with the convention kanna follows — deterministic output plus a -check flag so CI fails when committed code drifts from its source — and the classic codegen headaches (stale output, version skew, "works on my machine" generation) mostly evaporate.

This is why a solo developer shipping five generators is a signal and not just a flex. The fixed cost of building and distributing a Go code generator has collapsed. The go/packages API gives you typed ASTs; the toolchain gives you versioning and invocation; CI gives you drift detection. What used to justify a Google-scale project is now a weekend of work over your own codebase's conventions.

What to actually do with this

If you're running a Go service today, the move is not "adopt kanna." At v0.0.1 from a single maintainer, the syntax will churn, and the riskiest piece — kanna-orm, which generates queries, scanning, and relations for MySQL and Postgres — competes with sqlc, which is mature and battle-hardened. Use sqlc for the database layer. For DI, honestly, most services under a few hundred constructors are fine with hand-wired main.go; that's the option every DI framework marketing page forgets.

The pieces worth stealing are the small, low-blast-radius ones. Fixture generation (kanna-fixture fills struct fields with plausible fakes and takes override functions) and mapper generation (domain type to API type, both directions) are pure test-and-glue code — if the generator breaks or gets abandoned, the generated code still compiles and you just stop regenerating. Existing alternatives like goverter cover the mapping case with more mileage on them.

And if your codebase has its own recurring pattern — every team's does — the strongest version of this idea is writing your own generator against your own conventions, vendored in your own repo. A hundred-line tool using go/packages that knows exactly how your project names things beats a general-purpose tool with a struct-tag DSL you have to learn. The real cost of codegen was never writing the generator; it was distributing and versioning it, and that cost is now gone.

Be honest about the trade-offs that remain: generated files inflate diffs and PR review noise, generator runs need ordering when one tool's output feeds another, and every di:""-style struct tag is a tiny language your team must learn. Reflection didn't lose because it never works — it lost because when it breaks, there's nothing to read.

Kanna itself may join wire in the graveyard of Go codegen tools. The pattern it demonstrates won't. Compile-time generation keeps beating runtime cleverness in this ecosystem, and for the first time the toolchain treats that workflow as a first-class citizen. That's the durable takeaway — whoever's name is on the generator.

Sources & further reading

  1. I built five code generators to delete Go boilerplate — dev.to
  2. go-kanna/kanna — github.com
  3. Go 1.24 Release Notes — go.dev
  4. google/wire (archived) — github.com
Lenn Voss
Written by
Lenn Voss · Cloud & Infrastructure Writer

Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading