{
"$type": "site.standard.document",
"canonicalUrl": "https://rednafi.com/go/di-frameworks-bleh/",
"description": "Dependency injection in Go doesn't need Dig or Wire. Learn why manual wiring beats reflection magic and how Go's design makes DI frameworks overkill.",
"path": "/go/di-frameworks-bleh/",
"publishedAt": "2025-05-24T00:00:00.000Z",
"site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
"tags": [
"Go",
"Testing",
"API"
],
"textContent": "When working with Go in an [industrial programming] context, I feel like dependency\ninjection (DI) often gets a bad rep because of _DI frameworks_. But DI as a technique is\nquite useful. It just tends to get explained with too many OO jargons and triggers PTSD\namong those who came to Go to escape GoF theology.\n\n> Dependency Injection is a 25-dollar term for a 5-cent concept.\n>\n> -- James Shore\n\nDI basically means _passing values into a constructor instead of creating them inside it_.\nThat's really it. Observe:\n\nHere, NewServer creates its own DB. Instead, to inject the dependency, build DB\nelsewhere and pass it in as a constructor parameter:\n\nNow the constructor no longer decides how a database is built; it simply _receives_ one.\n\nIn Go, DI is often done using interfaces. You collate the behavior you care about in an\ninterface, and then provide different concrete implementations for different contexts. In\nproduction, you pass a real implementation of DB. In unit tests, you pass a fake\nimplementation that behaves the same way from the caller's perspective but avoids real\ndatabase calls.\n\nHere's how that looks:\n\nA real implementation of DB might look like this:\n\nAnd a fake implementation for unit tests might be:\n\nUse the fake in unit tests like so:\n\nThe compiler guarantees both RealDB and FakeDB satisfy DB, and during tests, we can\nswap out the implementations without much ceremony.\n\nWhy frameworks turn mild annoyance into actual pain\n\nOnce NewServer grows half a dozen dependencies, wiring them by hand can feel noisy. That's\nwhen a DI framework starts looking tempting.\n\nWith Uber's [dig], you register each constructor as a _provider_. Provide takes a\nfunction, uses reflection to inspect its parameters and return type, and adds it as a node\nin an internal dependency graph. Nothing is executed yet. Things only run when you call\n.Invoke() on the container.\n\nBut that reflection-driven magic is also where the pain starts. As your graph grows, it gets\nharder to tell which constructor feeds which one. Some constructor takes one parameter, some\ntakes three. There's no single place you can glance at to understand the wiring. It's all\nfigured out inside the container at runtime.\n\n> Let the container figure it out!\n>\n> -- every DI framework ever\n\nNow try commenting out NewFlagClient. The code [still compiles]. There's no error until\nruntime, when dig fails to construct NewService due to a missing dependency. And the error\nmessage you get?\n\nThat's five stack frames deep, far from where the problem started. Now you're digging\nthrough dig's internals to reconstruct the graph in your head.\n\nGoogle's [wire] takes a different approach: it shifts the graph-building to _code\ngeneration_. You collect your constructors in a wire.NewSet, call wire.Build, and the\ngenerator writes a wire_gen.go that wires everything up explicitly.\n\nComment out NewFlagClient and Wire fails earlier - during generation:\n\nIt's better than dig's runtime panic, but still comes with its own headaches:\n\n- You need to remember to run go generate ./... whenever constructor signatures change.\n- When something breaks, you're stuck reading through hundreds of lines of autogenerated\n glue to trace the issue.\n- You have to teach every teammate Wire's DSL - wire.NewSet, wire.Build, build tags, and\n sentinel rules. And if you ever switch to something different like dig, you'll need to\n learn a completely different set of concepts: Provide, Invoke, scopes, named values,\n etc.\n\nWhile DI frameworks tend to use vocabularies like _provider_ or _container_ to give you an\nessense of familiarity, they still reinvent the API surface every time. Switching between\nthem means relearning a new mental model.\n\nSo the promise of \"just register your providers and forget about wiring\" ends up trading\nclear, compile-time control for either reflection or hidden generator logic - and yet\nanother abstraction layer you have to debug.\n\nThe boring alternative: keep wiring explicit\n\nIn Go, you can just wire your own dependencies manually. Like this:\n\nLonger? Yes. But:\n\n- The call order is the dependency graph.\n- Errors are handled right where they happen.\n- If a constructor changes, the compiler points straight at every broken call:\n\n \n\nNo reflection, no generated code, no global state. Go type-checks the dependency graph early\nand loudly, exactly how it should be. And also, it doesn't confuse your LSP, so your IDE\nkeeps on being useful.\n\nIf main() really grows unwieldy, split _your_ code:\n\nEach helper is a regular function that anyone can skim without reading a framework manual.\nAlso, you usually build all of your dependency in one place and it's really not that big of\na deal if your builder function takes in 20 parameters and builds all the dependencies. Just\nput each function parameter on their own line and use gofumpt to format the code to make it\nreadable.\n\nReflection works elsewhere, so why not here?\n\nOther languages lean on containers because often times constructors cannot be overloaded and\ncompile times hurt. Go already gives you:\n\n- First-class functions so constructors are plain values.\n- Interfaces so implementations swap cleanly in tests.\n- Fast compilation so feedback loops stay tight.\n\nA DI framework often fixes problems Go already solved and trades away readability to do it.\n\n> The most magical thing about Go is how little magic it allows.\n>\n> -- Some Gopher on Reddit\n\nYou might still want a framework\n\nIt's tempting to make a blanket statement saying that you should never pick up a DI\nframework, but context matters here.\n\nI was watching [Uber's GopherCon talk on Go at scale] and how their DI framework [Fx] (which\nuses dig underneath) allows them to achieve consistency at scale. If you're Uber and have\nall the observability tools in place to get around the downsides, then you'll know.\n\nAlso, if you're working in a codebase that's already leveraging a framework and it works\nwell, then it doesn't make sense to refactor it without any incentives.\n\nOr, you're writing one of those languages where using a DI framework is the norm, and you'll\nbe called a weirdo if you try to reinvent the wheel there.\n\nHowever, in my experience, even in organizations that maintain a substantial number of Go\nrepos, DI frameworks add more confusion than they're worth. If your experience is otherwise,\nI'd love to be proven wrong.\n\n---\n\nThe post got a fair bit of discussion going around the web. You might find it interesting.\n\n- [hackernews]\n- [r/golang]\n- [r/experienceddevs]\n- [r/programming]\n\n\n\n\n[industrial programming]:\n https://peter.bourgon.org/go-for-industrial-programming/\n\n[dig]:\n https://github.com/uber-go/dig\n\n[still compiles]:\n https://go.dev/play/p/Vhimup7ukLo\n\n[wire]:\n https://github.com/google/wire\n\n[uber's gophercon talk on go at scale]:\n https://www.youtube.com/watch?v=nLskCRJOdxM&t\n\n[fx]:\n https://github.com/uber-go/fx\n\n[hackernews]:\n https://news.ycombinator.com/item?id=44086235\n\n[r/golang]:\n https://www.reddit.com/r/golang/comments/1kv0y1u/you_probably_dont_need_a_di_framework/\n\n[r/programming]:\n https://www.reddit.com/r/programming/comments/1kv0y2l/you_probably_dont_need_a_di_framework/\n\n[r/experienceddevs]:\n https://www.reddit.com/r/ExperiencedDevs/comments/1kv0y3n/you_probably_dont_need_a_di_framework/",
"title": "You probably don't need a DI framework"
}