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) and 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 make it easier Manage and inject dependencies efficiently.
Wire installation
We can execute the following command to install the Wire
tool:
go install github.com/google/wire/cmd/wire@latest
Please make sure you have added $GOPATH/bin
to the environment variable $PATH
before installation.
Wire usage
Pre-code preparation
Although we have installed the Wire
command line tool through the go install
command earlier, in a specific project, we still need to install the required for the project through the following command Wire
dependency in order to generate code in conjunction with the Wire
tool:
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 "..." // Simplified example 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 CreateApp
function that returns an App
instance or error
, where the App
instance is initialized All required dependencies are provided by the ProviderSet
component list, and ProviderSet
declares the acquisition/initialization methods of all possible required components, and also implies the order of dependencies between components.
The acquisition/initialization method of the component is called the “provider of the component” in Wire
There are a few more points to note:
-
The role of
wire.Build
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
). This part of the code will only be compiled 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 nils) has no meaning, just for compatibility with Go syntax.
2. Generate initialization code
Execute wire ./...
on the command line, 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 "..." // Simplified example 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 we 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. All unnecessary components will not generate corresponding initialization code.
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 // Provides instance constructors for all services 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, // References the instance constructor of all services NewApp, ) func CreateApp() (*App, error) { wire.Build(ProviderSet) // wire will be selectively initialized according to actual needs return nil, nil }
Core concepts of Wire
Wire
has two core concepts: providers (providers
) and injectors (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 is not limited to one. If necessary, an additional error
return value can be added.
If there are too many providers, we can also connect in groups, for example, combining post
related handler
and service
:
package handler var PostSet = wire.NewSet(NewPostHandler, service.NewPostService)
copy
Providers are grouped 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 function 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{} }
The InitializeApp
function is an injector. The function internally connects all providers through the wire.Build
function, and then returns & amp;gin.Engine{}
, this return value is not actually used, it is just to meet the requirements of the compiler and avoid errors. The real return value comes from ioc.NewGinEngineAndRegisterRoute
.
Reference
Go project essentials: Wire dependency injection tool in simple terms – Tencent Cloud Developer Community – Tencent Cloud
The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Go Skill TreeHomepage Overview 4426 people are learning the system