DEV Community

shanjunmei
shanjunmei

Posted on

Ditching Wire & Fx: I Built a Truly Go-Style Compile-Time DI Library

After years writing Go, I’ve clung to a core conviction:
Engineering optimization in Go shouldn’t be a series of compromising tradeoffs — it ought to be all-gain evolution.

This rings especially true for Dependency Injection (DI), the foundational engineering pillar for mid-to-large Go codebases. The two dominant mainstream DI solutions in the ecosystem force developers into an unavoidable binary compromise:

✅ Uber Fx

Boasts clean, elegant APIs and powerful modularization with a gentle learning curve, yet it relies heavily on runtime reflection.

  • Slow startup performance
  • Hidden runtime performance overhead in production
  • Missing dependencies, type mismatches and circular dependencies only surface via panics at launch time
  • Larger binary footprints It’s simply not viable for high-throughput services and foundational infrastructure components.

✅ Google Wire

Delivers compile-time code generation, zero reflection and zero runtime overhead — safe, performant, and fully aligned with Go’s static compilation philosophy.
Yet its developer experience is painfully counterintuitive:

  • Verbose, boilerplate-heavy syntax
  • Mandatory repetitive wire.NewSet declarations
  • Clunky empty return placeholder patterns
  • Severe restrictions on value injection (only constants supported natively)
  • No native nested module support, leading to messy structure as projects scale
  • No built-in Invoke hooks for post-initialization startup logic
  • Cryptic error reporting that makes dependency debugging a slog

For a long time, every project I worked on left me stuck between two bad choices:
Pick great DX and sacrifice runtime performance & stability; pick performance and safety while enduring a terrible developer workflow.

Since no existing tool hits every mark, I built my own.

Meet dig — a compile-time DI library that merges Fx’s ergonomic API with Wire’s compile-time safety, engineered from the ground up to fit Go’s design ethos.

Why Mainstream DI Libraries Are All Flawed

Many developers argue small Go projects don’t need DI; manual initialization works fine for trivial codebases.
But once you scale to multi-module layered architectures, microservices or monorepos, handwritten initialization becomes unmaintainable chaos:

  • Unmanaged tangled dependency graphs
  • Rampant global variables
  • Dispersed initialization logic scattered across packages
  • Tiny breaking changes triggering cascading failures
  • Near-impossible mocking for unit tests

DI is a non-negotiable necessity at scale — yet the two leading options carry fatal downsides:

1. Fx’s Critical Flaw: Runtime Uncertainty From Reflection

All dependency resolution and module composition in Fx happens via runtime reflection.
This means missing dependencies, circular references and type mismatches pass compilation silently, only crashing your service in production on startup.
For backend services and core infrastructure, this risk is completely unacceptable. On top of that, persistent reflection overhead slows service boot times and bloats binaries — directly violating Go’s core tenets of simplicity, efficiency and predictability.

2. Wire’s Critical Flaw: Maximum Performance, Minimal Developer Experience

Wire is Google’s official tooling with compile-time code generation, zero reflection and zero runtime dependencies, maximizing safety.
Its design, however, works against developer productivity:

  • Mandates maintaining dozens of repetitive wire.NewSet provider sets, creating massive boilerplate
  • wire.Value only accepts compile-time constants; runtime dynamic values cannot be injected
  • Modules can only be composed flatly, with no nested layering support — large repos devolve into disorganized messes
  • No native Invoke startup hooks; post-init logic requires handwritten wrappers
  • Obscure error messages that drastically slow down dependency troubleshooting

To sum up: Fx wins on developer experience but loses performance and safety; Wire delivers peak performance and safety at the cost of a painful workflow.
I set out to build something that delivers everything at once.

Core Mission of dig: Filling the Final Gap in Go’s DI Ecosystem

dig was built with a single, clear objective: avoid redundant reinvention, and deliver the optimal solution free of compromises.

  • Retain Wire’s core strengths: compile-time resolution, zero reflection, no runtime library dependencies
  • Match Fx’s developer experience: minimal, readable APIs and native modularization
  • Add missing capabilities absent from both libraries: enforced closure safety validation, full observability, flexible arbitrary value injection, native generics support, and granular unused component policies

The end result: compile-time safety guarantees, streamlined developer experience, and zero production runtime overhead.

Core Advantages of dig: Solving Every Pain Point of Traditional DI

1. Dual Safety Guarantees: Compile-Time Validation + Zero Runtime Cost

All dependency graph parsing, circular dependency detection and unused component validation run entirely during go generate.
dig outputs pure vanilla Go source code — no reflection, no proprietary runtime framework, and zero runtime dependency on the dig library itself.
The final binary matches the performance and startup speed of fully handwritten initialization logic, outperforming Fx by a wide margin.

2. Fx-Style Minimal API, Eliminating Wire’s Verbose Boilerplate

dig exposes only five core APIs with zero learning curve, each with clear, idiomatic Go semantics:
Build / Provide / Supply / Invoke / Module

  • Provide: Register constructor functions
  • Supply: Inject arbitrary values (constants, package-scoped variables, runtime dynamic values) — removes Wire’s constant-only injection limitation
  • Invoke: Execute startup logic once all dependencies are fully resolved
  • Module: Enable layered, nested modular composition

A minimal example to demonstrate its elegance:

//go:build digen
func InitApp() func(context.Context) error {
    return dig.Build(
        dig.Provide(NewConfig),
        dig.Provide(NewDB),
        dig.Supply(DefaultTimeout),
        dig.Invoke(func(srv *Server) error { return srv.Run() }),
    )
}
Enter fullscreen mode Exit fullscreen mode

3. Exclusive Closure Safety Mechanism to Eliminate Silent Bugs

Both Wire and Fx suffer from unvalidated closure capture risks, which frequently trigger elusive production runtime failures.
dig’s dedicated compiler enforces strict closure validation: capturing local variables defined inside the injector function InitApp is forbidden. Only package-level variables and literal values are permitted. This hard guard eliminates entire classes of silent hidden dependency bugs — a safety feature unique to dig among all mainstream Go DI libraries.

4. Full Observability Suite Beyond Basic Debug Logging

dig goes far beyond simple -debug print statements with end-to-end observability:

  • Compile-time: Three-tier unused component policies (error/ignore/drop) + package alias optimization to prune dead code upfront
  • Runtime: Global hookable Logf interface, seamlessly integrating with structured loggers like zap and logrus
  • Errors include full module hierarchy and complete dependency chains for pinpoint debugging, replacing opaque error outputs

5. Modern Go Engineering Support: Generics, Modularization & Conditional Logic

  • Native support for generic constructors and generic dependency injection, no manual wrapping required
  • Nested module layering built-in, perfectly suited for large monorepo multi-module architectures
  • Combined runtime conditional branching + compile build tag selection for maximum flexibility
  • Wrapper type utilities to resolve primitive type injection conflicts, eliminating identical-type dependency ambiguity errors

Side-by-Side Comparison Against Third-Party DI Tools

Capability dig Google Wire Uber Fx
Implementation Approach Compile-time code generation Compile-time code generation Runtime reflection
Zero runtime reflection
Zero runtime library dependencies
Compile-time error interception ❌ (Panics at runtime)
Minimal Fx-style clean API ❌ (Verbose boilerplate)
Mandatory closure safety validation
Flexible arbitrary value injection ✅ (All value types) ❌ (Constants only)
Granular unused component policies ✅ (3 configurable modes) ❌ (Only deletion)

My Thoughts: What Go DI Should Truly Look Like

Many developers resist DI in Go — their aversion stems from heavy Java-style frameworks, invasive abstractions and opaque runtime magic.
Go’s design philosophy centers on simplicity, transparency, predictability and zero hidden behavior:

  • Fx violates predictability with opaque runtime reflection magic
  • Wire violates simplicity with verbose, friction-heavy syntax

dig exists to return to Go’s foundational principles:
No runtime black-box magic; all behavior fully transparent and traceable. No redundant boilerplate; every design decision prioritizes developer efficiency.
It is not built for the sake of reinventing wheels, but to resolve real-world production pain points:
Lightweight for small projects, structured and controllable for massive monorepos — striking a perfect balance between performance, safety and developer experience.

Quick Integration & Open Source Repository

Stable Version: v1.0.9
Minimum Go Version: Go 1.21+
License: MIT (Free for commercial use)

Installation Commands:

go get github.com/shanjunmei/dig@v1.0.9
go install github.com/shanjunmei/dig/cmd/digen@latest
Enter fullscreen mode Exit fullscreen mode

🔥 GitHub Repository: https://github.com/shanjunmei/dig
Comprehensive documentation, sample projects, migration guides and advanced usage examples are all available, with active ongoing maintenance. Star, file Issues or submit PRs to join the discussion!

Closing Remarks

There is no universally perfect technical choice, but there are options that align far better with a language’s core philosophy.
dig was not created to replace Wire or Fx — it offers Go developers a new, zero-compromise, universally adaptable DI alternative:
It delivers Wire’s extreme performance and compile-time safety, paired with Fx’s elegant developer experience and modular power, plus exclusive built-in safety validations and full observability tooling.

If you’ve grown frustrated with Fx’s reflection overhead or Wire’s clunky syntax, give dig a try to experience dependency injection truly built for Go’s design language.

Feel free to test the library, share feedback, submit feature requests, and collaborate to refine a DI tool tailored to the Go ecosystem!

Top comments (0)