1. Preface
I’ve been working with Golang for a while and found that Golang also needs a dependency injection framework similar to Spring in Java. If the project is small, having a dependency injection framework or not is not a big deal. But when the project gets bigger, it is necessary to have a proper dependency injection framework. Through research, we learned that the main dependency injection tools used in Golang are Inject, Dig, and so on. But today, we are going to introduce Wire, a compile-time dependency injection tool developed by the Go team.
2. What is Dependency Injection (DI)?
Speaking of dependency injection
brings up another term, inversion of control
(IoC), which is a design idea whose core purpose is to reduce the coupling of code. Dependency injection is a design pattern that implements inversion of control
and is used to solve dependency problems.
For example, suppose our code is layered with a dal layer that connects to a database and is responsible for reading and writing to the database. Then the service above our dal layer is responsible for calling the dal layer to process the data, which in our current code might look like this.
|
|
In this code, the hierarchical dependency is service -> dal -> db, and the upstream hierarchy instantiates the dependency via Getxxx
. But in real production, our dependency chain is less vertical and more horizontal dependencies. That is, we may have to call the Getxxx
method multiple times in one method, which makes our code extremely uncomplicated.
Not only that, but our dependencies are written dead, i.e., the dependents’ generation relationships are written dead in the dependents’ code. When the generation of the dependent changes, we also need to change the function of the dependent, which greatly increases the amount of modified code and the risk of errors.
Next we use dependency injection
to transform the code.
|
|
As in the above coding case, we achieve inter-level dependency injection by injecting the db instance object into the dal and then injecting the dal instance object into the service. Some of the dependencies are decoupled.
In the case of a simple system and a small amount of code, the above implementation is not a problem. But when the project becomes large and the relationship between structures becomes very complex, manually creating each dependency and assembling them layer by layer becomes extremely tedious and error-prone. That’s where warrior wire
comes in!
3. Wire Come
3.1 Introduction
Wire is a lightweight dependency injection tool for Golang. It was developed by the Go Cloud team and does dependency injection at compile time by automatically generating code. It does not require a reflection mechanism, as you will see later, and Wire generates code as if it were handwritten.
3.2 Quick use
Installation of wire.
|
|
The above command will generate an executable program wire
in $GOPATH/bin
, which is the code generator. You can add $GOPATH/bin
to the system environment variable $PATH
, so you can execute the wire
command directly from the command line.
Let’s see how to use wire
in an example.
Now we have three such types.
The init method of all three.
Assume that Channel
has a GetMsg
method and BroadCast
has a Start
method.
If we write the code manually, we should write it as follows.
If we use wire
, what we need to do becomes the following.
-
extract an init method InitializeBroadCast.
-
Write a wire.go file for the wire tool to parse dependencies and generate code.
Note: You need to add build constraints to the file header:
//+build wireinject
-
Using the wire tool, generate the code by executing the command:
wire gen wire.go
in the directory where wire.go is located.The following code will be generated, which is the Init function that is actually used when compiling the code.
We tell
wire
theinit
methods of the various components we use (NewBroadCast
,NewChannel
,NewMessage
), then thewire
tool will automatically derive dependencies based on the function signatures (parameter type/return value type/function name) of those methods.Both
wire.go
andwire_gen.go
files have a+build
in the header position, but one is followed bywireinject
and the other by!wireinject
.+build
is actually a feature of the Go language. Similar to C/C++ conditional compilation, when executinggo build
you can pass in some options that determine whether certain files are compiled or not. Thewire
tool will only process files withwireinject
, so we’ll add this to ourwire.go
file. The generatedwire_gen.go
is for us to use,wire
doesn’t need to handle it, hence the!wireinject
.
3.3 Basic Concepts
Wire
has two basic concepts, Provider
(constructor) and Injector
(injector)
-
Provider
is actually the normal method that generates the component, these methods take the required dependencies as parameters, create the component and return it. TheNewBroadCast
in our example above is theProvider
. -
Injector
can be understood as a connector toProviders
, which is used to callProviders
in the order of dependencies and eventually return the build target. TheInitializeBroadCast
in our example above is theInjector
.
4. Wire usage in practice
The following is a brief introduction to the application of wire
in the Fishu questionnaire form service.
The project
module of the flybook questionnaire form service initializes the handler, service and dal layers by means of parameter injection to achieve dependency inversion. All external dependencies are initialized via BuildInjector
injector.
4.1 Basic usage
The dal pseudocode is as follows.
|
|
The service pseudocode is as follows.
|
|
The handler pseudo code is as follows.
|
|
The injector.go pseudocode is as follows.
Defined in wire.go as follows.
|
|
Execute wire gen . /internal/app/wire.go
to generate wire_gen.go.
|
|
Add the method app.BuildInjector
to main.go to initialize the injector.
Note that if you run it with an “InitializeEvent redeclared in this block” exception, then check for a blank line between your //+build wireinject
and the line package app
, this blank line must be there! See https://github.com/google/wire/issues/117.
4.2 Advanced features
4.2.1 NewSet
NewSet
is generally used when there are a lot of initialized objects, to reduce the information in the Injector
. When our project becomes large, we can imagine that there will be a lot of Providers. NewSet
helps us to group these Providers according to business relationships and form ProviderSet
(constructor set), which can be used later.
4.2.2 Structs
The Provider
s in the above examples are all functions. In addition to functions, structures can also act as Provider
s. Wire
gives us the Struct Constructor (Struct Provider). A structure constructor creates a structure of some type and then fills its fields with parameters or calls other constructors.
|
|
4.2.3 Bind
The purpose of the Bind
function is to allow dependencies of interface types to participate in the construction of Wire
. The construction of Wire
relies on parameter types, which are not supported by interface types. The Bind
function achieves dependency injection by binding an interface type to an implementation type.
4.2.4 CleanUp
The constructor can provide a cleanup function that will be called if subsequent constructors return a failure. This cleanup function is available after initializing Injector
. Typical application scenarios for the cleanup function are file resources and network connection resources. The cleanup function is usually used as a second return value, with parameters of type func()
. When any of Provider
has a cleanup function, Injector
must also include it in the return value of the function. And Wire
has the following restrictions on the number and order of return values for Provider
.
- The first return value is the object to be generated.
- If there are 2 return values, the second return value must be func() or error.
- If there are 3 return values, the second return value must be func(), and the third return value must be error.
|
|
For more information on usage, please refer to the official wire guide:https://github.com/google/wire/blob/main/docs/guide.md
4.3 Advanced Use
We then use these wire
advanced features above to adapt the project
service to code.
project_dal.go.
|
|
dal.go
project_service.go
|
|
service.go
The handler pseudo code is as follows.
|
|
The injector.go pseudocode is as follows.
wire.go
|
|
5. Precautions
5.1 Same type problem
wire does not allow different injected objects to have the same type. google officially considers this case to be a design flaw. In this case, the types of objects can be distinguished by type aliases.
For example, the service will operate on two Redis instances at the same time, RedisA & RedisB.
In this case, wire cannot derive the dependency relationship. This can be implemented as follows.
5.2 The Singleton Problem
The essence of dependency injection is to use a singleton to bind the mapping relationship between the interface and the objects that implement it. Inevitably, some objects are stateful in practice, and the same type of object always changes in different use case scenarios, so single cases can cause data errors and fail to preserve each other’s state. For this scenario we usually design multi-layer DI containers to achieve single instance isolation, or to manage the life cycle of the object by itself without DI containers.
6. Conclusion
Wire is a powerful dependency injection tool. Unlike Inject, Dig, etc., Wire only generates code instead of injecting it at runtime using reflection, so you don’t have to worry about performance loss. Wire can be a great tool to help us build and assemble complex objects during project engineering.
For more information about Wire, please go to: https://github.com/google/wire