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)?
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
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
$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.
Channel has a
GetMsg method and
BroadCast has a
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:
Using the wire tool, generate the code by executing the command:
wire gen wire.goin 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.
initmethods of the various components we use (
NewMessage), then the
wiretool will automatically derive dependencies based on the function signatures (parameter type/return value type/function name) of those methods.
wire_gen.gofiles have a
+buildin the header position, but one is followed by
wireinjectand the other by
+buildis actually a feature of the Go language. Similar to C/C++ conditional compilation, when executing
go buildyou can pass in some options that determine whether certain files are compiled or not. The
wiretool will only process files with
wireinject, so we’ll add this to our
wire.gofile. The generated
wire_gen.gois for us to use,
wiredoesn’t need to handle it, hence the
3.3 Basic Concepts
Wire has two basic concepts,
Provider (constructor) and
Provideris actually the normal method that generates the component, these methods take the required dependencies as parameters, create the component and return it. The
NewBroadCastin our example above is the
Injectorcan be understood as a connector to
Providers, which is used to call
Providersin the order of dependencies and eventually return the build target. The
InitializeBroadCastin our example above is the
4. Wire usage in practice
The following is a brief introduction to the application of
wire in the Fishu questionnaire form service.
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
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.
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
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.
Providers in the above examples are all functions. In addition to functions, structures can also act as
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.
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.
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
- 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.
The handler pseudo code is as follows.
The injector.go pseudocode is as follows.
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.
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