There’s a classic concept in compiler design called “bootstrapping”—a compiler that compiles itself. It sounds like circular dependency, but it’s actually the standard path for compilers and code generation tools to mature.
dig is a compile‑time dependency injection tool that achieves zero runtime overhead through code generation. And this project manages its own dependencies with itself—meaning the initialization code inside the dig project is generated by dig itself.
That sounds a bit circular, but there are several practical reasons behind it: validating that the tool actually works, proving that “zero runtime dependency” isn’t just marketing, and providing users with a real‑world usage example.
What Exactly Is “Bootstrapping”?
The term “bootstrapping” comes from compiler design. Simply put: a tool uses its own output to build itself.
The Go compiler is written in Go—that’s bootstrapping. The Rust compiler is written in Rust. For compilers and code generation tools, this is a natural evolutionary path: when a tool matures enough, it becomes capable of managing itself.
For dig, bootstrapping means: the dependency injection logic inside the dig project is implemented using code generated by dig itself.
Why Bootstrap a Tool?
1. Validate That the Tool Actually Works
There’s a term called “dogfooding”—using your own product to build itself. If dig doesn’t use dig, why should users believe it can manage complex dependency graphs?
Every time we modify dig’s code and regenerate its own DI code, we immediately see whether:
- The API is intuitive
- The generated code is correct
- Error messages are clear
- There’s any performance regression
If a tool can’t be used to build itself, don’t expect others to use it.
2. Prove That “Zero Runtime Dependency” Isn’t Empty Talk
One of dig’s core promises is that generated code is plain Go code with no runtime dependency on the dig library. Bootstrapping is the perfect proof: the code generated inside the dig project is completely independent of the dig runtime.
If it couldn’t do this, the promise would be broken. But it can.
3. Provide the Most Authentic Reference Example
Users wondering how to organize a large project with dig? Just look at how dig uses dig itself. The project’s internal code is the most authentic and authoritative example available.
How Does It Work?
dig’s bootstrapping implementation lives in the cmd/digen/ directory, centered around three files.
First, there’s a hand‑written dependency definition. A di.go file describes all component dependencies:
//go:build digen
//go:generate go run -mod=mod github.com/shanjunmei/dig/cmd/digen
func InitApp(cfg *config.Config) func(context.Context) error {
return dig.Build(
dig.Provide(logger.NewLogger),
dig.Provide(loader.NewPackageLoader),
dig.Provide(app.NewApp),
dig.Provide(generator.NewGenerator),
dig.Provide(processor.NewProcessor),
dig.Invoke(func(a *app.App) error { return a.Run() }),
)
}
The key mechanism is build tag switching. di.go has //go:build digen, making it visible only during the generation phase. Meanwhile dig_gen.go, the generated code, carries //go:build !digen and is only used during normal compilation. The same InitApp function has different implementations depending on whether you’re running go generate or go build.
When go generate runs, digen reads the declarations in di.go, analyzes the dependency graph, and generates complete initialization code. The generated code computes the correct dependency order and initializes everything in topological order:
// dig_gen.go (generated code)
func InitApp(cfg *config.Config) func(context.Context) error {
v0 := cfg
v1 := loader.NewPackageLoader()
v2 := logger.NewLogger(v0)
// ... dependency order automatically computed by digen
v7 := app.NewApp(v6, v1, v2, v5, v0)
return func(ctx context.Context) error {
return dig_invoke_1(v7)
}
}
A few interesting details: anonymous functions are lifted to package‑level functions, named sequentially as dig_provider_1, dig_provider_2… Dependencies are assigned variable names like v0, v1, v2… in topological order. This shows that digen fully analyzes the dependency graph and determines the optimal initialization order before generating any code.
The Bootstrapping Loop
The complete bootstrapping flow:
- A hand‑written
InitAppdefinition indi.go(the initial version) go generaterunsdigen, which parsesdi.goand generatesdig_gen.gogo buildcompilesdigenusing the initialization code fromdig_gen.go- The newly built
digenbinary can regeneratedig_gen.go - When code changes, the process repeats
Once this loop is established, there’s no need to manually maintain initialization order—digen computes everything automatically.
Does It Actually Make Things Better?
Yes, and in two ways.
For developers: Every code change can be validated by running digen and seeing if the tool still works correctly. Dependency injection adjustments no longer require manually maintaining initialization order—the tool handles it.
For users: Bootstrapping itself is the best trust signal—a tool that doesn’t use itself is hard to trust. At the same time, the project’s internal code serves as a real‑world usage example, more convincing than any documentation.
If you’re building a similar code generation tool, consider bootstrapping it. The process forces you to polish the tool to a genuinely usable state, and it sends users a strong signal: this tool is trustworthy.
Rather than analyzing various “selling points” of a tool, let the tool prove itself—being able to use itself is the best proof.
dig project: github.com/shanjunmei/dig