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), 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 manage and Inject dependencies.

Wire installation

We can install the Wire tool by executing the following command:

go install github.com/google/wire/cmd/wire@latest

Please ensure that $GOPATH/bin has been added to the environment variable $PATH before installation.

Wire use

Preparation of code

Although we have installed the command line tool through the go install command before, in the specific project , we still need to install the dependencies required by the project through the following command in order to generate code with the tool: Wire WireWire

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 "..."  // 简化示例

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  function that returns an instance of App or error ,  All dependencies required for instance initialization are provided by the component list  , and  declares the acquisition/initialization methods of all possible required components, which also implies the component dependency order. CreateAppAppProviderSetProviderSet

The acquisition/initialization method of the component is called the "provider of the component" in Wire

There are a few more points to note:

  • wire.BuildThe role of 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), which only This part of the code will be compiled only 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 nil) has no meaning, just for compatibility with Go syntax.
   2. Generate initialization code

Command line execution wire ./..., 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 "..."  // 简化示例

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 you 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, and all unnecessary components will be initialized. No corresponding initialization code is generated.

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

// 提供了所有 service 的实例构造器
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,  // 引用了所有 service 的实例构造器
	NewApp,
)

func CreateApp() (*App, error) {
	wire.Build(ProviderSet)  // wire 会按照实际需要,选择性地进行初始化
	return nil, nil
}

Core concepts of Wire

Wire has two core concepts: provider (providers) and injector (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 of is not limited to one. If necessary, an additional return value of error can be added.

If there are too many providers, we can also connect in groups, for example, post related handler and service to combine:

package handler

var PostSet = wire.NewSet(NewPostHandler, service.NewPostService)

copy

Group providers 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 role 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{}
}

InitializeAppThe function is an injector. The function internally connects all providers through the wire.Build function, and then returns &gin.Engine{}. This return value is not actually used. Yes, it is just to meet the requirements of the compiler and avoid reporting 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

Guess you like

Origin blog.csdn.net/FENGQIYUNRAN/article/details/134335204