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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// dal/user.go

func (u *UserDal) Create(ctx context.Context, data *UserCreateParams) error {
    db := mysql.GetDB().Model(&entity.User{})
    user := entity.User{
      Username: data.Username,
      Password: data.Password,
   }

    return db.Create(&user).Error
}

// service/user.go
func (u *UserService) Register(ctx context.Context, data *schema.RegisterReq) (*schema.RegisterRes, error) {
   params := dal.UserCreateParams{
      Username: data.Username,
      Password: data.Password,
   }

   err := dal.GetUserDal().Create(ctx, params)
   if err != nil {
      return nil, err
   }

   registerRes := schema.RegisterRes{
      Msg: "register success",
   }

   return &registerRes, nil
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// dal/user.go
type UserDal struct{
    DB *gorm.DB
}

func NewUserDal(db *gorm.DB) *UserDal{
    return &UserDal{
        DB: db
    }
}

func (u *UserDal) Create(ctx context.Context, data *UserCreateParams) error {
    db := u.DB.Model(&entity.User{})
    user := entity.User{
      Username: data.Username,
      Password: data.Password,
   }

    return db.Create(&user).Error
}

// service/user.go
type UserService struct{
    UserDal *dal.UserDal
}

func NewUserService(userDal dal.UserDal) *UserService{
    return &UserService{
        UserDal: userDal
    }
}

func (u *UserService) Register(ctx context.Context, data *schema.RegisterReq) (*schema.RegisterRes, error) {
   params := dal.UserCreateParams{
      Username: data.Username,
      Password: data.Password,
   }

   err := u.UserDal.Create(ctx, params)
   if err != nil {
      return nil, err
   }

   registerRes := schema.RegisterRes{
      Msg: "register success",
   }

   return &registerRes, nil
}

// main.go 
db := mysql.GetDB()
userDal := dal.NewUserDal(db)
userService := dal.NewUserService(userDal)

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.

1
go get github.com/google/wire/cmd/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.

1
2
3
4
5
6
7
type Message string
type Channel struct {
    Message Message
}
type BroadCast struct {
    Channel Channel
}

The init method of all three.

1
2
3
4
5
6
7
8
9
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}
}

Assume that Channel has a GetMsg method and BroadCast has a Start method.

1
2
3
4
5
6
7
8
func (c Channel) GetMsg() Message {
    return c.Message
}

func (b BroadCast) Start() {
    msg := b.Channel.GetMsg()
    fmt.Println(msg)
}

If we write the code manually, we should write it as follows.

1
2
3
4
5
6
7
func main() {
    message := NewMessage()
    channel := NewChannel(message)
    broadCast := NewBroadCast(channel)

    broadCast.Start()
}

If we use wire, what we need to do becomes the following.

  1. extract an init method InitializeBroadCast.

    1
    2
    3
    4
    5
    
    func main() {
        b := demo.InitializeBroadCast()
    
        b.Start()
    }
    
  2. Write a wire.go file for the wire tool to parse dependencies and generate code.

    1
    2
    3
    4
    5
    6
    7
    8
    
    //+build wireinject
    
    package demo
    
    func InitializeBroadCast() BroadCast {
        wire.Build(NewBroadCast, NewChannel, NewMessage)
        return BroadCast{}
    }
    

    Note: You need to add build constraints to the file header: //+build wireinject

  3. 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.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    // 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 tell wire the init methods of the various components we use (NewBroadCast, NewChannel, NewMessage), then the wire tool will automatically derive dependencies based on the function signatures (parameter type/return value type/function name) of those methods.

    Both wire.go and wire_gen.go files have a +build in the header position, but one is followed by wireinject and the other by !wireinject. +build is actually a feature of the Go language. Similar to C/C++ conditional compilation, when executing go build you can pass in some options that determine whether certain files are compiled or not. The wire tool will only process files with wireinject, so we’ll add this to our wire.go file. The generated wire_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. The NewBroadCast in our example above is the Provider.

  • Injector can be understood as a connector to Providers, which is used to call Providers in the order of dependencies and eventually return the build target. The InitializeBroadCast in our example above is the Injector.

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func NewProjectDal(db *gorm.DB) *ProjectDal{
    return &ProjectDal{
        DB:db
    }
}

type ProjectDal struct {
   DB *gorm.DB
}

func (dal *ProjectDal) Create(ctx context.Context, item *entity.Project) error {
   result := dal.DB.Create(item)
   return errors.WithStack(result.Error)
}
// QuestionDal、QuestionModelDal...

The service pseudocode is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func NewProjectService(projectDal *dal.ProjectDal, questionDal *dal.QuestionDal, questionModelDal *dal.QuestionModelDal) *ProjectService {
   return &projectService{
      ProjectDal:       projectDal,
      QuestionDal:      questionDal,
      QuestionModelDal: questionModelDal,
   }
}

type ProjectService struct {
   ProjectDal       *dal.ProjectDal
   QuestionDal      *dal.QuestionDal
   QuestionModelDal *dal.QuestionModelDal
}

func (s *ProjectService) Create(ctx context.Context, projectBo *bo.ProjectCreateBo) (int64, error) {}

The handler pseudo code is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func NewProjectHandler(srv *service.ProjectService) *ProjectHandler{
    return &ProjectHandler{
        ProjectService: srv
    }
}

type ProjectHandler struct {
   ProjectService *service.ProjectService
}

func (s *ProjectHandler) CreateProject(ctx context.Context, req *project.CreateProjectRequest) (resp *
project.CreateProjectResponse, err error) {}

The injector.go pseudocode is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func NewInjector()(handler *handler.ProjectHandler) *Injector{
    return &Injector{
        ProjectHandler: handler
    }
}

type Injector struct {
   ProjectHandler *handler.ProjectHandler
   // components,others...
}

Defined in wire.go as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// +build wireinject

package app

func BuildInjector() (*Injector, error) {
   wire.Build(
      NewInjector,

      // handler
      handler.NewProjectHandler,

      // services
      service.NewProjectService,
      // 更多service...

      //dal
      dal.NewProjectDal,
      dal.NewQuestionDal,
      dal.NewQuestionModelDal,
      // 更多dal...

      // db
      common.InitGormDB,
      // other components...
   )

   return new(Injector), nil
}

Execute wire gen . /internal/app/wire.go to generate wire_gen.go.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

func BuildInjector() (*Injector, error) {
   db, err := common.InitGormDB()
   if err != nil {
      return nil, err
   }
   
   projectDal := dal.NewProjectDal(db)
   questionDal := dal.NewQuestionDal(db)
   questionModelDal := dal.NewQuestionModelDal(db)
   projectService := service.NewProjectService(projectDal, questionDal, questionModelDal)
   projectHandler := handler.NewProjectHandler(projectService)
   injector := NewInjector(projectHandler)
   return injector, nil
}

Add the method app.BuildInjector to main.go to initialize the injector.

1
2
3
4
5
6
7
8
injector, err := BuildInjector()
if err != nil {
   return nil, err
}

//project service start
svr := projectservice.NewServer(injector.ProjectHandler, logOpt)
svr.Run()

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.

1
2
3
4
5
6
7
8
9
// project.go
var ProjectSet = wire.NewSet(NewProjectHandler, NewProjectService, NewProjectDal)

// wire.go
func BuildInjector() (*Injector, error) {
   wire.Build(InitGormDB, ProjectSet, NewInjector)

   return new(Injector), nil
}

4.2.2 Structs

The Providers in the above examples are all functions. In addition to functions, structures can also act as Providers. 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// project_service.go
// func provider
func NewProjectService(projectDal *dal.ProjectDal, questionDal *dal.QuestionDal, questionModelDal *dal.QuestionModelDal) *ProjectService {
   return &projectService{
      ProjectDal:       projectDal,
      QuestionDal:      questionDal,
      QuestionModelDal: questionModelDal,
   }
}

// Equivalent to
wire.Struct(new(ProjectService), "*") // "*" means all fields are injected

// is also equivalent to
wire.Struct(new(ProjectService), "ProjectDal", "QuestionDal", "QuestionModelDal")

// If individual properties do not want to be injected, then the struct definition can be modified.
type App struct {
    Foo *Foo
    Bar *Bar
    NoInject int `wire:"-"`
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// project_dal.go
type IProjectDal interface {
   Create(ctx context.Context, item *entity.Project) (err error)
   // ...
}

type ProjectDal struct {
   DB *gorm.DB
}

var bind = wire.Bind(new(IProjectDal), new(*ProjectDal))

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.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// db.go
func InitGormDB()(*gorm.DB, func(), error) {
    // Initialize db connection
    // ...
    cleanFunc := func(){
        db.Close()
    }

    return db, cleanFunc, nil
}

// wire.go
func BuildInjector() (*Injector, func(), error) {
   wire.Build(
      common.InitGormDB,
      // ...
      NewInjector
   )

   return new(Injector), nil, nil
}

// Generated wire_gen.go
func BuildInjector() (*Injector, func(), error) {
   db, cleanup, err := common.InitGormDB()
   // ...
   return injector, func(){
       // All provider cleanup functions will be here
       cleanup()
   }, nil
}

// main.go
injector, cleanFunc, err := app.BuildInjector()
defer cleanFunc()

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
type IProjectDal interface {
   Create(ctx context.Context, item *entity.Project) (err error)
   // ...
}

type ProjectDal struct {
   DB *gorm.DB
}

// Struct method is the constructor provided by wire, "*" means inject values for all fields, here you can use "DB" instead
// Bind method binds the interface to the implementation
var ProjectSet = wire.NewSet(
   wire.Struct(new(ProjectDal), "*"),
   wire.Bind(new(IProjectDal), new(*ProjectDal)))


func (dal *ProjectDal) Create(ctx context.Context, item *entity.Project) error {}

dal.go

1
2
3
4
5
// DalSet dal injection
var DalSet = wire.NewSet(
   ProjectSet,
   // QuestionDalSet、QuestionModelDalSet...
)

project_service.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type IProjectService interface {
   Create(ctx context.Context, projectBo *bo.CreateProjectBo) (int64, error)
   // ...
}

type ProjectService struct {
   ProjectDal       dal.IProjectDal
   QuestionDal      dal.IQuestionDal
   QuestionModelDal dal.IQuestionModelDal

}
func (s *ProjectService) Create(ctx context.Context, projectBo *bo.ProjectCreateBo) (int64, error) {}

var ProjectSet = wire.NewSet(
   wire.Struct(new(ProjectService), "*"),
   wire.Bind(new(IProjectService), new(*ProjectService)))

service.go

1
2
3
4
5
6

// ServiceSet service injection
var ServiceSet = wire.NewSet(
   ProjectSet,
   // other service set...
)

The handler pseudo code is as follows.

1
2
3
4
5
6
7
8
var ProjectHandlerSet = wire.NewSet(wire.Struct(new(ProjectHandler), "*"))

type ProjectHandler struct {
   ProjectService service.IProjectService
}

func (s *ProjectHandler) CreateProject(ctx context.Context, req *project.CreateProjectRequest) (resp *
project.CreateProjectResponse, err error) {}

The injector.go pseudocode is as follows.

1
2
3
4
5
6
var InjectorSet = wire.NewSet(wire.Struct(new(Injector), "*"))

type Injector struct {
   ProjectHandler *handler.ProjectHandler
   // others...
}

wire.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 // +build wireinject

package app


func BuildInjector() (*Injector, func(), error) {
   wire.Build(
      // db
      common.InitGormDB,
      // dal
      dal.DalSet,
      // services
      service.ServiceSet,
      // handler
      handler.ProjectHandlerSet,
      // injector
      InjectorSet,
      // other components...
   )

   return new(Injector), nil, nil
}

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.

1
2
func NewRedisA() *goredis.Client {...}
func NewRedisB() *goredis.Client {...}

In this case, wire cannot derive the dependency relationship. This can be implemented as follows.

1
2
3
4
5
type RedisCliA *goredis.Client
type RedisCliB *goredis.Client

func NewRedisA() RedicCliA {...}
func NewRedisB() RedicCliB {...}

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