Skip to content

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

  • wire as code generation, not a runtime container
  • Providers (constructors) · provider sets · injectors (wire.Build)
  • Binding interfaces with wire.Bind; struct providers with wire.Struct
  • Build tags: //go:build wireinject and the generated wire_gen.go

📖 Reading / Sources

📝 Notes

  • wire generates 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. wire matches 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. wire replaces that stub with a real implementation in wire_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): the func() is a cleanup, and wire chains cleanups in reverse construction order — solving the "who calls Close()" lifecycle gap from yesterday → [[cleanup]].
  • wire changes 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

wire is 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 wireinject on 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 *memRepo but a consumer wanting the interface; got "no provider found for AccountRepository". Added wire.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)

  1. Q: Is wire a runtime DI container? What does it actually produce?
ANo. `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.
  1. Q: A constructor returns *memRepo but a consumer needs the AccountRepository interface. 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)