Day 129 — Dependency Injection with wire¶
Month 5 · Week 3 · ⬅ Day 128 · Day 130 ➡ · Journal index
🎯 Learning Objective¶
Generate the composition root automatically with google/wire — compile-time, reflection-free DI — and understand providers, provider sets, and the wire.Build injector.
📚 Topics¶
wireas code generation, not a runtime container- Providers (constructors) · provider sets · injectors (
wire.Build) - Binding interfaces with
wire.Bind; struct providers withwire.Struct - Build tags:
//go:build wireinjectand the generatedwire_gen.go
📖 Reading / Sources¶
-
google/wireREADME & User Guide - Go blog — Compile-time Dependency Injection With Wire
-
wirebest practices -
go generatedirective
📝 Notes¶
wiregenerates the wiring code you'd otherwise hand-write (Day 128). At build time it reads your providers and emits an ordinary Go function — zero reflection, zero runtime cost. If wiring is wrong, it fails at code-gen / compile, not at startup → [[codegen]].- A provider is just a constructor:
func NewLedger(r AccountRepository) *Ledger.wirematches a provider's return type to another provider's parameter type to order the graph → [[providers]]. - A provider set (
wire.NewSet(...)) groups related providers so you reference one symbol instead of listing every constructor in every injector → [[provider-set]]. - An injector is a function whose body is a single
wire.Build(...)call, guarded by//go:build wireinject.wirereplaces that stub with a real implementation inwire_gen.go(built only without the tag) → [[injector]]. - Constructors return concrete types, but consumers want interfaces → use
wire.Bind(new(AccountRepository), new(*memRepo))to tell wire which concrete satisfies which port → [[wire-bind]]. - Providers can return
(T, func(), error): thefunc()is a cleanup, and wire chains cleanups in reverse construction order — solving the "who calls Close()" lifecycle gap from yesterday → [[cleanup]]. wirechanges nothing about your design: the same constructors work with hand-wiring. It only removes the boilerplate of a large composition root → [[composition-root]].
💻 Code Examples¶
wireis third-party (github.com/google/wire), so this is a snippet, not a runnable stdlib example. The hand-written equivalent is yesterday's example.
//go:build wireinject
// +build wireinject
package main
import "github.com/google/wire"
// Provider set: the constructors that build the graph.
var ledgerSet = wire.NewSet(
NewMemRepo, // *memRepo
wire.Bind(new(AccountRepository), new(*memRepo)), // *memRepo satisfies the port
NewLedger, // needs AccountRepository -> *Ledger
)
// Injector STUB. wire replaces the body in wire_gen.go; this file is excluded
// from normal builds by the wireinject tag.
func InitLedger() *Ledger {
wire.Build(ledgerSet)
return nil // unreachable; wire rewrites this
}
// Generated wire_gen.go (conceptually) — plain, readable Go you could've typed:
func InitLedger() *Ledger {
repo := NewMemRepo()
ledger := NewLedger(repo)
return ledger
}
Regenerate with go generate ./... after adding //go:generate wire near the injector.
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
| (Conceptual) Map yesterday's hand-wiring to a provider set | ✅ | reasoning only — wire needs the module |
| Reuse the injected-port exercise | ✅ | exercises/month-05/week-3/ports |
🐛 Mistakes Made¶
- Forgot
//go:build wireinjecton the injector file, so the stub and the generated function both compiled → "InitLedger redeclared". The build tag is what keeps them apart. - Gave wire a constructor returning a concrete
*memRepobut a consumer wanting the interface; got "no provider found for AccountRepository". Addedwire.Bind.
❓ Open Questions¶
- For a service with two DBs of the same type, how do you disambiguate providers? (Likely distinct named types or
wire.Value— to confirm.)
🧠 Active Recall (answer without looking)¶
- Q: Is
wirea runtime DI container? What does it actually produce?
A
No. `wire` is a *compile-time code generator*: it reads your providers and emits an ordinary Go function (`wire_gen.go`) that constructs the graph. No reflection, no runtime container — wiring errors surface at code-gen/compile time.- Q: A constructor returns
*memRepobut a consumer needs theAccountRepositoryinterface. How do you tell wire they connect?
A
`wire.Bind(new(AccountRepository), new(*memRepo))` inside the provider set — it declares that `*memRepo` satisfies the `AccountRepository` port, so wire can supply the concrete where the interface is required.🪶 Feynman Reflection¶
wire is a robot that writes the boring part of main for me. I hand it a pile of constructors; it figures out that this one's output is that one's input, sorts them, and types out the assembly code I would have written by hand — but it never gets the order wrong and it complains before the program runs if a piece is missing. Crucially, it's still just plain Go at the end; delete wire and the generated file keeps working.
🕳️ Knowledge Gaps¶
- Cleanup-function chaining order in larger graphs, and combining provider sets across packages.
✅ Summary¶
I understand wire as compile-time DI codegen: providers map output→input, provider sets group them, wire.Build marks the injector, wire.Bind connects concretes to ports, and cleanup funcs handle lifecycles — all producing ordinary Go.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 130: domain modeling — value objects, entities, and making invalid states unrepresentable.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: docs(journal): wire compile-time DI notes (day 129)