When the number of instance dependencies (components) in a project increases, it will be a very cumbersome task to manually write initialization code and maintain dependencies between components, especially in large warehouses. Therefore, there are already many dependency injection frameworks in the community.
In addition to Wire from Google, there are also Dig (Uber), Inject(Facebook). Both Dig and Inject are implemented based on Golang's Reflection. This not only affects performance, but also the dependency injection mechanism is opaque to users and is very "black box".
Clear is better than clever ,Reflection is never clear.
— Rob Pike
In contrast, Wire is entirely based on code generation. During the development phase, wire will automatically generate the initialization code of the component. The generated code is human-readable and can be submitted to the warehouse or compiled normally. Therefore, Wire's dependency injection is very transparent and does not cause any performance loss in the running phase.
Wire
Wire
is a code generation tool designed specifically for dependency injection (Dependency Injection
). It can automatically generate code for initializing various dependencies, thereby helping us manage and Inject dependencies.
Wire installation
We can install the Wire
tool by executing the following command:
go install github.com/google/wire/cmd/wire@latest
Please ensure that $GOPATH/bin
has been added to the environment variable $PATH
before installation.
Wire use
Preparation of code
Although we have installed the command line tool through the go install
command before, in the specific project , we still need to install the dependencies required by the project through the following command in order to generate code with the tool: Wire
Wire
Wire
go get github.com/google/wire@latest
1.Create wire.go file
Before generating code, we declare the dependencies and initialization order of each component. Create a wire.go file at the application entry point.
// +build wireinject
package main
import "..." // 简化示例
var ProviderSet = wire.NewSet(
configs.Get,
databases.New,
repositories.NewUser,
services.NewUser,
NewApp,
)
func CreateApp() (*App, error) {
wire.Build(ProviderSet)
return nil, nil
}
This file will not participate in compilation, but is only used to tell Wire the dependencies of each component and the expected generation results. In this file: We expect Wire to generate a function that returns an instance of App
or error
, All dependencies required for instance initialization are provided by the component list , and declares the acquisition/initialization methods of all possible required components, which also implies the component dependency order. CreateApp
App
ProviderSet
ProviderSet
The acquisition/initialization method of the component is called the "provider of the component" in Wire
There are a few more points to note:
-
wire.Build
The role of is to connect or bind all the initialization functions we defined previously. When we run thewire
tool to generate code, it will automatically create and inject the required instances based on these dependencies.The first line of the file must be commented with
//go:build wireinject
or// +build wireinject
(used in versions beforego 1.18
), which only This part of the code will be compiled only when using thewire
tool, and will be ignored in other cases. - In this file, editors and IDEs may not be able to provide code hints, but that's okay, I'll show you how to fix this later.
- The return of
CreateApp
(two nil) has no meaning, just for compatibility with Go syntax.
2. Generate initialization code
Command line execution wire ./...
, and then you will get the automatically generated code file below.
cmd/web/wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package main
import "..." // 简化示例
func CreateApp() (*App, error) {
conf, err := configs.Get()
if err != nil {
return nil, err
}
db, err := databases.New(conf)
if err != nil {
return nil, err
}
userRepo, err := repositories.NewUser(db)
if err != nil {
return nil, err
}
userSvc, err := services.NewUser(userRepo)
if err != nil {
return nil, err
}
app, err := NewApp(userSvc)
if err != nil {
return nil, err
}
return app, nil
}
3. Use initialization code
Wire has generated the real CreateApp
initialization method for us, and you can use it directly now.
cmd/web/main.go
// main.go
func main() {
app := CreateApp()
app.Run()
}
Components are loaded on demand
Wire has an elegant feature. No matter how many component providers are passed in wire.Build
, Wire will always initialize components according to actual needs, and all unnecessary components will be initialized. No corresponding initialization code is generated.
Therefore, we can provide as many providers as possible when using them, and leave the job of selecting components to Wire. In this way, whether we reference new components or discard old components during development, we do not need to modify the code of the initialization step wire.go.
For example, you can provide all instance constructors in the services layer.
pkg/services/wire.go
package services
// 提供了所有 service 的实例构造器
var ProviderSet = wire.NewSet(NewUserService, NewFeedService, NewSearchService, NewBannerService)
In initialization, reference as many possible component providers as possible.
cmd/web/wire.go
var ProviderSet = wire.NewSet(
configs.ProviderSet,
databases.ProviderSet,
repositories.ProviderSet,
services.ProviderSet, // 引用了所有 service 的实例构造器
NewApp,
)
func CreateApp() (*App, error) {
wire.Build(ProviderSet) // wire 会按照实际需要,选择性地进行初始化
return nil, nil
}
Core concepts of Wire
Wire
has two core concepts: provider (providers
) and injector (injectors
).
Wire providers
Provider: A function that can produce a value, that is, a function that returns a value. For example, the NewPostHandler
function in the entry code:
func NewPostHandler(serv service.IPostService) *PostHandler {
return &PostHandler{serv: serv}
}
copy
The return value of is not limited to one. If necessary, an additional return value of error
can be added.
If there are too many providers, we can also connect in groups, for example, post
related handler
and service
to combine:
package handler
var PostSet = wire.NewSet(NewPostHandler, service.NewPostService)
copy
Group providers using the wire.NewSet
function, which returns a ProviderSet
structure. Not only that, wire.NewSet
can also group multiple ProviderSet
`wire.NewSet(PostSet, XxxSet)
`
For the previous InitializeApp
function, we can upgrade it like this:
//go:build wireinject
package wire
func InitializeAppV2() *gin.Engine {
wire.Build(
handler.PostSet,
ioc.NewGinEngineAndRegisterRoute,
)
return &gin.Engine{}
}
Then use the Wire
command to generate code, which is consistent with the previous result.
Wire injectors
The role of the injector (injectors
) is to connect all providers (providers
). Review our previous code:
func InitializeApp() *gin.Engine {
wire.Build(
handler.NewPostHandler,
service.NewPostService,
ioc.NewGinEngineAndRegisterRoute,
)
return &gin.Engine{}
}
InitializeApp
The function is an injector. The function internally connects all providers through the wire.Build
function, and then returns &gin.Engine{}
. This return value is not actually used. Yes, it is just to meet the requirements of the compiler and avoid reporting errors. The real return value comes from ioc.NewGinEngineAndRegisterRoute
.