Go project essentials: A simple introduction to the Wire dependency injection tool

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 the wire 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 before go 1.18). This part of the code will only be compiled when using the wire 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