
Inversion of Control (IoC) is a design principle in object-oriented programming used to reduce the degree of coupling between computer code. The most common form of inversion of control is dependency injection (Dependency Injection, DI), and Wire is a common dependency injection tool.
Basic concepts
Inversion of control is essentially a design philosophy of handing control away: in the past, you created an object yourself with new in your code, decided when it was created, what served as its dependencies, and how the call chain was assembled; with IoC, this control over “creation + assembly + scheduling” is handed to the outside (a framework/container/upper layer). Your business code only describes “what I need and what I provide” and is not responsible for stitching everything together, which can greatly reduce the coupling of the code.
Dependency injection is one of the most common forms of IoC: when A needs B, A does not need to create B itself; B is created externally and passed in as a parameter, or injected through a method parameter or a field. DI makes dependency relationships more explicit.
Wire is a compile-time dependency injection code generation tool for Go. Its purpose is to combine the providers you declare (that is, constructor functions) with the injector (that is, the assembly entry function), and to automatically generate initialization code equivalent to calling new and passing parameters layer by layer by hand. You first define a ProviderSet, for example ProviderSet = wire.NewSet(NewData, NewRepo, NewUsecase, NewService, etc.), and then call wire.Build in an injector function such as wireApp to express the object you ultimately want, for example, “I want a kratos.App”. During generation, Wire infers dependencies from the type signatures of these constructors, computes the correct creation order, checks whether dependencies are missing and whether circular dependencies exist, and finally generates wire_gen.go, which contains a truly runnable wireApp implementation and also stitches together the cleanup functions needed to release resources. At runtime no container and no reflection are needed; it is essentially writing the assembly code automatically, making initialization clearer, safer, and easier to maintain.
Why reduce coupling?
Improve maintainability and prevent a change in one place from affecting everything else.
Improve testability: tests are more precisely targeted, exercising only the logic itself, which better fits today’s design philosophy.
Improve replaceability and lower the cost of code iteration; perhaps only one parameter needs to be replaced to swap the database, without major changes.
Lower coupling can also improve a team’s collaboration efficiency, since each person only needs to care about their own part.
Improve project reliability: when one part fails, the failure is less likely to spread, and problems are easier to identify.
Inversion of Control
Common approaches to inversion of control
-
Constructor injection: dependencies are passed in as constructor parameters; the dependency relationships are the most explicit, and this is the most recommended approach in Go.
-
Method/Setter injection: injected through a setter or an initialization method; suited to optional dependencies or runtime replacement.
-
Field/property injection: the framework stuffs dependencies into struct fields through reflection/tags; it saves effort when writing, but dependencies are hidden and testability is poor.
-
Interface + Factory: isolate concrete implementations behind an interface, then let a factory take responsibility for creating the implementation objects; commonly used to “choose an implementation by configuration”.
-
Abstract Factory / Builder: used when object assembly is very complex and needs to be done in steps or with multiple combination schemes.
-
Service Locator: the object itself fetches dependencies from a “container” by key/type (this is also IoC), but the dependencies are opaque, so it is generally not recommended as the primary approach.
-
Plugin / Registry: implementations are attached to a registry through a registration mechanism and are discovered and loaded at runtime by name/capability.
-
Functional Options: common in Go; use
WithXxx()to inject optional dependencies or configuration, suited to constructors with many parameters. -
Context/Request-scoped Injection: each request carries its dependencies (such as trace/span, user information, transaction objects); common in web framework middleware chains.
-
Codegen Wiring: dependencies are resolved at compile time and assembly code is generated, with no container and no reflection at runtime (tools like Wire and Dagger).
Dependency Injection
Common dependency injection tools
Go
-
Compile time: Google Wire
-
Runtime containers: Uber dig, sarulabs/di, (older) facebookgo/inject
-
Application-framework-style lifecycle: Uber fx (built on dig, with OnStart/OnStop)
-
Other approaches: hand-written assembly in main, Functional Options (not a tool, but very common)
Java / JVM
-
Spring / Spring Boot (runtime container, annotation-driven)
-
Guice (runtime container)
-
Dagger (mainly compile-time, common on Android)
-
Micronaut (leans compile-time / reduces reflection)
.NET
-
Microsoft.Extensions.DependencyInjection (official built-in DI)
-
Autofac, Castle Windsor (third-party containers)
Node.js / TypeScript
-
NestJS (the framework ships with DI)
-
InversifyJS, tsyringe, typedi (container/injection libraries)
Python
-
dependency-injector (container)
-
FastAPI Depends (closer to “dependency resolution/function injection”, with very strong request-scoped support)
-
Injector (Guice-style)
Rust / C++ (less “containerization”, more manual/compile-time)
-
Rust: manual assembly + trait abstraction is more common (relatively few DI tools)
-
C++: Boost.DI (compile-time)
Wire
There are many dependency injection frameworks in the Go community. For example, Uber’s dig and Facebook’s inject both use reflection to perform runtime dependency injection.
wire is a dependency injection tool open-sourced by Google that completes dependency injection at compile time by automatically generating code.
wire has two core concepts: providers and injectors.
Provider
A provider is usually a constructor whose name starts with New; it represents the implementation of a certain struct, that is, the creation of an object.
package demo
import (
"context"
"errors"
)
type A struct {
Value int
}
func NewA(ctx context.Context, b B) (A, error) {
if b.Value == 0 {
return A{}, errors.New("cannot provide a when value is zero")
}
return A{Value: b.Value + 2}, nil
}
Provider functions can be grouped into provider sets. The wire.NewSet function can be used to add multiple provider functions to a single set. This is very useful when several provider functions are often used together.
package demo
import (
// ...
"github.com/google/wire"
)
var ProviderSet = wire.NewSet(NewData, NewRepo, NewUsecase, NewService)
At the same time, other provider sets can also be added to a provider set.
Injector
An application uses an injector to connect providers; an injector is simply something that calls the providers in dependency order.
To declare an injector function, you only need to call wire.Build in the function body. The return values of this function do not matter either, as long as their types are correct. These values will be ignored in the generated code.
func wireApp(*conf.Server, *conf.Data, log.Helper) (*kratos.App, func(), error) {
panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
}
Installing and using wire
Install the dependency package with install in golang, and then run the wire command in the same directory as the injector to complete automatic dependency injection.
go install github.com/google/wire/cmd/wire@latest
cd cmd/golist
wire
The example above generates a wire_gen.go file, at which point dependency injection is complete.
// Code generated by Wire. DO NOT EDIT.
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package main
import (
"golist/internal/biz"
"golist/internal/conf"
"golist/internal/data"
"golist/internal/server"
"golist/internal/service"
"github.com/go-kratos/kratos/v2"
"github.com/go-kratos/kratos/v2/log"
_ "go.uber.org/automaxprocs"
)
// Injectors from wire.go:
// wireApp init kratos application.
func wireApp(confServer *conf.Server, confData *conf.Data, logger log.Logger) (*kratos.App, func(), error) {
db, err := data.NewDB(confData)
if err != nil {
return nil, nil, err
}
dataData, cleanup, err := data.NewData(db, logger)
if err != nil {
return nil, nil, err
}
todoRepo := data.NewTodoRepo(dataData, logger)
todoUsecase := biz.NewTodoUsecase(todoRepo, logger)
todoService := service.NewTodoService(todoUsecase)
grpcServer := server.NewGRPCServer(confServer, todoService, logger)
httpServer := server.NewHTTPServer(confServer, todoService, logger)
app := newApp(logger, grpcServer, httpServer)
return app, func() {
cleanup()
}, nil
}