Introduction
As Go services grow beyond simple CRUD handlers, managing dependencies — database connections, configuration, loggers, HTTP clients — becomes increasingly complex. Passing everything through constructor parameters works for small projects but becomes unwieldy at scale. Dependency injection (DI) containers solve this by automating the wiring between components, reducing boilerplate and making your codebase more testable.
This article compares three Go DI approaches: Google Wire (14,402⭐) — compile-time code generation, Uber FX (7,578⭐) — runtime framework with lifecycle management, and samber/do (2,755⭐) — a lightweight generics-based container. We compare their philosophies, APIs, and real-world usage patterns.
Feature Comparison
| Feature | Google Wire | Uber FX | samber/do |
|---|---|---|---|
| Approach | Compile-time code gen | Runtime reflection | Runtime generics |
| GitHub Stars | 14,402 | 7,578 | 2,755 |
| Lifecycle Hooks | Manual (provider cleanup) | OnStart/OnStop | Shutdown callbacks |
| Error Detection | Compile-time | Runtime (on invoke) | Runtime (on invoke) |
| Interface Binding | Yes (explicit) | Yes (annotated) | Yes (implicit) |
| Lazy Loading | No (all initialized) | Yes (via annotated) | Yes (lazy provider) |
| Module System | Provider sets | fx.Module | No (flat) |
| Configuration Injection | Manual | fxconfig integration | Manual |
| Generated Code | ~200 lines wiring | 0 lines | 0 lines |
| Learning Curve | High (DSL syntax) | Medium | Low (intuitive) |
| Go Generics Required | No | No | Yes (1.18+) |
Google Wire: Compile-Time Safety
Google Wire takes the most radical approach: it’s a code generator, not a runtime library. You define providers (functions that create dependencies) and Wire generates the wiring code. The generated code is regular Go — you can read it, debug it, and it compiles with zero reflection overhead.
| |
After running wire, the generated wire_gen.go:
| |
Wire’s compile-time approach catches dependency cycles, missing providers, and type mismatches before your code runs. If you add a new dependency to UserService, Wire will refuse to generate code until all providers are satisfied. This eliminates an entire class of runtime errors.
For interface binding:
| |
Uber FX: Runtime Framework with Lifecycle
Uber FX is a full-featured application framework built around DI. Beyond constructor injection, it provides lifecycle management (startup hooks, graceful shutdown), configuration injection, and a module system for organizing large codebases.
| |
FX’s module system is particularly powerful for large applications:
| |
FX supports value groups for plugin architectures — multiple providers can contribute values to a shared slice:
| |
samber/do: Lightweight Generics-Based Container
samber/do takes a minimalist approach — a single container struct that uses Go generics for type-safe resolution without any code generation or reflection. It’s the simplest to adopt if you want DI without framework overhead.
| |
samber/do supports named providers, health checks, and lazy loading:
| |
For interface-based programming:
| |
Choosing the Right DI Approach
Use Google Wire when: You prioritize compile-time safety above all else. Large teams benefit from Wire’s explicit dependency graphs — CI catches wiring errors before deployment. The learning curve is steeper but pays off in production reliability for complex service graphs.
Use Uber FX when: You want a batteries-included framework with lifecycle management, configuration injection, and module organization. FX shines in microservice architectures where services need startup health checks, graceful shutdown, and clear separation of concerns. Uber itself uses FX across hundreds of microservices.
Use samber/do when: You need simple, zero-magic DI with minimal learning curve. The generics-based API is intuitive for Go developers. Ideal for small-to-medium services that don’t need framework-level lifecycle management. The health check feature is surprisingly useful for Kubernetes liveness probes.
Migration Patterns
If you’re starting with manual dependency injection and want to adopt a container:
Start with samber/do — the lowest barrier. Wrap your existing constructors with
do.Provide()calls. No code generation step, no build changes.Graduate to FX — when you need lifecycle management, configuration injection, or module organization. FX’s
fx.Supply()lets you register already-constructed values alongside provider functions.Consider Wire — for high-reliability services where dependency misconfiguration in production is unacceptable. The switch to Wire requires a build step but the generated code is zero-overhead.
FAQ
Do I really need a DI container in Go?
For small services (under 10 dependencies), manual constructor injection works fine. DI containers become valuable when: (1) you have 20+ dependencies with multi-level wiring, (2) you frequently swap implementations for testing vs production, or (3) you need lifecycle management for graceful startup/shutdown.
How does DI affect testing?
It simplifies testing dramatically. With Wire, generate a test injector that substitutes mock providers. With FX, use fx.Replace() to swap implementations. With samber/do, use do.Override() to inject test doubles. All three approaches avoid global state, making parallel tests safe.
What’s the performance overhead of each approach?
Wire has zero runtime overhead — the generated code is plain Go function calls. FX has minor reflection overhead at startup (milliseconds for typical graphs); runtime resolution uses cached results. samber/do uses generics, which compile to concrete types with no reflection cost; the container map lookup has minimal overhead.
Can I mix these DI approaches in one project?
Technically yes, but avoid it. Each container manages its own lifecycle and dependency graph. Mixing them creates confusion about which container owns which dependency. Pick one approach per service or bounded context.
How do these compare to manual dependency injection in large Go projects?
Manual DI in large projects leads to main() functions with 50+ lines of constructor calls, implicit ordering requirements, and fragile startup sequences. A container makes the dependency graph explicit, ensures correct initialization order, and provides a single place to manage service lifecycle. The trade-off is an additional dependency and (for Wire) a code generation step in the build pipeline.
Why Self-Host Your DI Container Strategy?
Choosing the right DI approach is a foundational architecture decision that affects your entire service’s maintainability, testability, and operational reliability. Embedding this decision in your self-hosted Go services means your dependency management strategy is version-controlled, reviewable, and consistent across your codebase.
For microservice architecture patterns, see our self-hosted microservices frameworks guide. For project structure at scale, check our monorepo build systems comparison. For automated code generation workflows, see our code generation tools guide.
💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com