1. Preface
I have been in contact with Golang for a while and found that Golang also needs a dependency injection framework similar to Spring in Java. If the project scale is relatively small, it is not a big problem whether there is a dependency injection framework, but when the project becomes larger, it is very necessary to have a suitable dependency injection framework. Through research, we learned that the commonly used dependency injection tools in Golang are mainly Inject, Dig, etc. But today we are mainly introducing Wire developed by the Go team, a tool that implements dependency injection during compilation.
2. What is Dependency Injection (DI)
Speaking of dependency injection, it is necessary to introduce another noun control inversion (IoC). IoC is a design idea, and its core role is to reduce the coupling degree of code. Dependency injection is a design pattern that implements control inversion and solves dependency problems.
For example, suppose that our code hierarchy relationship is a dal layer connecting the database, which is responsible for the read and write operations of the database. Then the service on our dal layer is responsible for calling the dal layer to process data. In our current code, it might look like this:
// dal/ func (u *UserDal) Create(ctx , data *UserCreateParams) error { db := ().Model(&{}) user := { Username: , Password: , } return (&user).Error } // service/ func (u *UserService) Register(ctx , data *) (*, error) { params := { Username: , Password: , } err := ().Create(ctx, params) if err != nil { return nil, err } registerRes := { Msg: "register success", } return ®isterRes, nil }
In this code, the hierarchical dependency is service -> dal -> db, and the upstream hierarchy passesGetxxx
Instantiate dependencies. But in actual production, our dependency chains are less vertical dependencies, and more horizontal dependencies. That is, in one method, we may need to call it multiple timesGetxxx
This makes our code extremely unconcise.
Not only that, our dependencies are written to death, that is, the dependant's code writes the generation relationship of the dependant's dependency. When the generation method of the dependant is changed, we also need to change the dependant's function, which greatly increases the amount of code modification and the risk of errors.
Next, we use dependency injection to modify the code:
// dal/ type UserDal struct{ DB * } func NewUserDal(db *) *UserDal{ return &UserDal{ DB: db } } func (u *UserDal) Create(ctx , data *UserCreateParams) error { db := (&{}) user := { Username: , Password: , } return (&user).Error } // service/ type UserService struct{ UserDal * } func NewUserService(userDal ) *UserService{ return &UserService{ UserDal: userDal } } func (u *UserService) Register(ctx , data *) (*, error) { params := { Username: , Password: , } err := (ctx, params) if err != nil { return nil, err } registerRes := { Msg: "register success", } return ®isterRes, nil } // db := () userDal := (db) userService := (userDal)
As in the above encoding situation, we implement inter-hierarchical dependency injection by injecting the db instance object into dal and then injecting the dal instance object into service. Decoupled part of the dependencies.
The above implementation method is indeed no problem when the system is simple and the amount of code is small. But when the project is so large that the relationship between structures becomes very complex, the way each dependency is created manually and then assembled layer by layer will become extremely cumbersome and prone to errors. At this time, the Warriors Wire appeared!
3. Wire Come
3.1 Introduction
Wire is a lightweight Golang dependency injection tool. It is developed by the Go Cloud team and it completes dependency injection during the compilation period by automatically generating code. It does not require a reflection mechanism. You will see later that the code generated by Wire is no different from handwriting.
3.2 Quick use
Wire installation:
go get /google/wire/cmd/wire
The above command will be$GOPATH/bin
Generate an executable program inwire
, this is the code generator. Can$GOPATH/bin
Add system environment variables$PATH
, so it can be executed directly on the command linewire
Order.
Let's take a look at how to use it in an examplewire
。
Now we have three types like this:
type Message string type Channel struct { Message Message } type BroadCast struct { Channel Channel }
The init method of the three:
func NewMessage() Message { return Message("Hello Wire!") } func NewChannel(m Message) Channel { return Channel{Message: m} } func NewBroadCast(c Channel) BroadCast { return BroadCast{Channel: c} }
Suppose Channel has a GetMsg method and BroadCast has a Start method:
func (c Channel) GetMsg() Message { return } func (b BroadCast) Start() { msg := () (msg) }
If we write the code manually, our writing method should be:
func main() { message := NewMessage() channel := NewChannel(message) broadCast := NewBroadCast(channel) () }
If usedwire
, what we need to do becomes the following work:
1. Extract an init method InitializeBroadCast:
func main() { b := () () }
2. Write a file for wire tool to parse dependencies and generate code:
//+build wireinject package demo func InitializeBroadCast() BroadCast { (NewBroadCast, NewChannel, NewMessage) return BroadCast{} }
Note: You need to add build constraints in the file header://+build wireinject
3. Use the wire tool to generate code and execute commands in the directory where you are located:wire gen
. The following code will be generated, that is, the Init function that is actually used when compiling the code:
// Code generated by Wire. DO NOT EDIT. //go:generate wire //+build !wireinject func InitializeBroadCast() BroadCast { message := NewMessage() channel := NewChannel(message) broadCast := NewBroadCast(channel) return broadCast }
We tellwire
, the various components we useinit
method(NewBroadCast
, NewChannel
, NewMessage
),Sowire
The tool will automatically deduce dependencies based on the function signatures of these methods (parameter type/return value type/function name).
and
wire_gen.go
There is one position at the head of the file+build
, but one iswireinject
, the other one is!wireinject
。+build
In fact, it is a feature of the Go language. Conditional compilation similar to C/C++, executiongo build
You can pass in some options to determine whether some files are compiled based on this option.wire
Tools will only handlewireinject
file, so ourThe file needs to be added. Generated
wire_gen.go
It's for us to use.wire
No processing is required, so there is!wireinject
。
3.3 Basic concepts
Wire
There are two basic concepts.Provider
(Constructor) andInjector
(Injector)
-
Provider
In fact, it is the ordinary method of generating components. These methods receive the required dependencies as parameters, create the component and return it. Our example aboveNewBroadCast
that isProvider
。 -
Injector
It can be understood asProviders
connector, which is used to call in dependency orderProviders
and finally return to the build target. Our example aboveInitializeBroadCast
that isInjector
。
4. Wire usage practice
Here is a brief introductionwire
Application in Feishu Questionnaire Form Service.
Feishu Questionnaire Form Serviceproject
In the module, the initialization of the handler layer, service layer and dal layer is achieved through parameter injection to achieve dependency inversion. passBuildInjector
The injector is used to initialize all external dependencies.
4.1 Basic use
The pseudocode of dal is as follows:
func NewProjectDal(db *) *ProjectDal{ return &ProjectDal{ DB:db } } type ProjectDal struct { DB * } func (dal *ProjectDal) Create(ctx , item *) error { result := (item) return () } // QuestionDal、QuestionModelDal...
The service pseudocode is as follows:
func NewProjectService(projectDal *, questionDal *, questionModelDal *) *ProjectService { return &projectService{ ProjectDal: projectDal, QuestionDal: questionDal, QuestionModelDal: questionModelDal, } } type ProjectService struct { ProjectDal * QuestionDal * QuestionModelDal * } func (s *ProjectService) Create(ctx , projectBo *) (int64, error) {}
The handler pseudocode is as follows:
func NewProjectHandler(srv *) *ProjectHandler{ return &ProjectHandler{ ProjectService: srv } } type ProjectHandler struct { ProjectService * } func (s *ProjectHandler) CreateProject(ctx , req *) (resp * , err error) {}
The pseudo-code is as follows:
func NewInjector()(handler *) *Injector{ return &Injector{ ProjectHandler: handler } } type Injector struct { ProjectHandler * // components,others... }
In the following definition:
// +build wireinject package app func BuildInjector() (*Injector, error) { ( NewInjector, // handler , // services , // More service... //dal , , , // More dal... // db , // other components... ) return new(Injector), nil }
implementwire gen ./internal/app/
Generate wire_gen.go
// Code generated by Wire. DO NOT EDIT. //go:generate wire //+build !wireinject func BuildInjector() (*Injector, error) { db, err := () if err != nil { return nil, err } projectDal := (db) questionDal := (db) questionModelDal := (db) projectService := (projectDal, questionDal, questionModelDal) projectHandler := (projectService) injector := NewInjector(projectHandler) return injector, nil }
Add the method to initialize the injector in
injector, err := BuildInjector() if err != nil { return nil, err } //Project service startssvr := (, logOpt) ()
Note that if you run, it appearsBuildInjector
Redefine, then check yours//+build wireinject
andpackage app
Whether there is a blank line between these two lines, this blank line must be! See /google/wire/issues/117
4.2 Advanced Features
4.2.1 NewSet
NewSet
Generally, when there are many initialization objects, it will be reduced.Injector
Information. When our project is huge to a certain extent, it is conceivable that there will be a lot of providers.NewSet
Help us group these Providers according to business relationships and formProviderSet
(Constructor collection), you only need to use this collection in the future.
// var ProjectSet = (NewProjectHandler, NewProjectService, NewProjectDal) // func BuildInjector() (*Injector, error) { (InitGormDB, ProjectSet, NewInjector) return new(Injector), nil }
4.2.2 Struct
The above exampleProvider
All are functions, except functions, structures can also act asProvider
The role.Wire
Provide us withStructural constructor(Struct Provider). The structure constructor creates a structure of a certain type and then fills its fields with parameters or calling other constructors.
// project_service.go // Function providerfunc NewProjectService(projectDal *, questionDal *, questionModelDal *) *ProjectService { return &projectService{ ProjectDal: projectDal, QuestionDal: questionDal, QuestionModelDal: questionModelDal, } } // Equivalent to(new(ProjectService), "*") // "*" represents all field injection // It is also equivalent to(new(ProjectService), "ProjectDal", "QuestionDal", "QuestionModelDal") // If individual attributes do not want to be injected, you can modify the struct definition:type App struct { Foo *Foo Bar *Bar NoInject int `wire:"-"` }
4.2.3 Bind
Bind
The function is to allow interface type dependencies to participateWire
Construction.Wire
The construction of rely relies on parameter types, and interface types are not supported.Bind
Functions achieve the purpose of dependency injection by binding interface types and implementation types.
// project_dal.go type IProjectDal interface { Create(ctx , item *) (err error) // ... } type ProjectDal struct { DB * } var bind = (new(IProjectDal), new(*ProjectDal))
4.2.4 CleanUp
The constructor can provide a cleanup function. If the subsequent constructor returns, the cleanup function returned by the previous constructor will be called. InitializationInjector
After that, this cleaning function can be obtained. Typical application scenarios of the cleaning function are file resources and network connection resources. The cleanup function is usually used as the second return value, and the parameter type isfunc()
. whenProvider
Any one of them has a cleanup function,Injector
The function return value must also include the function. AndWire
rightProvider
The number and order of return values are as follows:
- 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
// func InitGormDB()(*, func(), error) { // Initialize db link // ... cleanFunc := func(){ () } return db, cleanFunc, nil } // func BuildInjector() (*Injector, func(), error) { ( , // ... NewInjector ) return new(Injector), nil, nil } // Generated wire_gen.gofunc BuildInjector() (*Injector, func(), error) { db, cleanup, err := () // ... return injector, func(){ // All provider cleaning functions will be here cleanup() }, nil } // injector, cleanFunc, err := () defer cleanFunc()
For more details, please refer to the official guide for wire:/google/wire/blob/main/docs/
4.3 Advanced use
Then we use the abovewire
Advanced Featuresproject
Services are code-renovated:
project_dal.go
type IProjectDal interface { Create(ctx , item *) (err error) // ... } type ProjectDal struct { DB * } // The method is a constructor provided by wire. "*" means injecting values into all fields. Here you can use "DB" instead// Method binds interface and implementationvar ProjectSet = ( (new(ProjectDal), "*"), (new(IProjectDal), new(*ProjectDal))) func (dal *ProjectDal) Create(ctx , item *) error {}
// DalSet dal Injectionvar DalSet = ( ProjectSet, // QuestionDalSet、QuestionModelDalSet... )
project_service.go
type IProjectService interface { Create(ctx , projectBo *) (int64, error) // ... } type ProjectService struct { ProjectDal QuestionDal QuestionModelDal } func (s *ProjectService) Create(ctx , projectBo *) (int64, error) {} var ProjectSet = ( (new(ProjectService), "*"), (new(IProjectService), new(*ProjectService)))
// ServiceSet service injectionvar ServiceSet = ( ProjectSet, // other service set... )
The handler pseudocode is as follows:
var ProjectHandlerSet = ((new(ProjectHandler), "*")) type ProjectHandler struct { ProjectService } func (s *ProjectHandler) CreateProject(ctx , req *) (resp * , err error) {}
The pseudo-code is as follows:
var InjectorSet = ((new(Injector), "*")) type Injector struct { ProjectHandler * // others... }
// +build wireinject package app func BuildInjector() (*Injector, func(), error) { ( // db , // dal , // services , // handler , // injector InjectorSet, // other components... ) return new(Injector), nil, nil }
5. Things to note
5.1 The same type of problem
wire does not allow different injection objects to have the same type. Google officials believe that this situation is a design flaw. In this case, the type of the object can be distinguished by type alias.
For example, the service will operate two Redis instances at the same time, RedisA & RedisB
func NewRedisA() * {...} func NewRedisB() * {...}
For this case, wire cannot deduce a dependency relationship. This can be implemented like this:
type RedisCliA * type RedisCliB * func NewRedisA() RedicCliA {...} func NewRedisB() RedicCliB {...}
5.2 Singleton Problem
The essence of dependency injection is to use singletons to bind interfaces and implement mapping relationships between interface objects. In practice, some objects are stateful. Objects of the same type always change in different use case scenarios. Singletons will cause data errors and cannot save each other's states. For this scenario, we usually design multi-layer DI containers to achieve singleton isolation, or to manage the life cycle of the object by itself.
6. Conclusion
Wire is a powerful dependency injection tool. Unlike Inject, Dig, etc., Wire only generates code instead of using reflection to inject at runtime, so there is no need to worry about performance losses. During the project engineering process, Wire can help us to build and assemble complex objects well.
For more information about Wire, please send to:/google/wire
This is the end of this article about the tutorial on using Wire, the official Go dependency injection tool. For more related Go languages, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!