Why migrate dig to wire

Posted by billy2shoe on Mon, 03 Jan 2022 21:27:08 +0100

Beginning

Both dig and wire are injection-dependent tools for Go, so why switch from dig to wire when the framework is essentially similar in functionality?

scene

Let's start with the scene.

Suppose our project hierarchy is router->controller->service->dao.

It's probably like this:

Now we need to expose an interface to an order service.

Look at main on the first page. Go file.

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "github.com/wuqinqiang/digvswire/dig"
    "github.com/wuqinqiang/digvswire/router"
)

func main() {
    serverStart()
}

func serverStart() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("init app err:%v\n", err)
        }
    }()
    e := gin.Default()
    di := dig.ContainerByDig()
    err := router.RegisterRouter(e, di)
    if err != nil {
        fmt.Printf("register router err:%v", err)
    }
    _ = e.Run(":8090")
}

The gin startup project is used here. Then we look at dig.ContainerByDig(),

dig

package dig

import (
    "github.com/wuqinqiang/digvswire/controller"
    "github.com/wuqinqiang/digvswire/dao"
    "github.com/wuqinqiang/digvswire/server"
    "go.uber.org/dig"
)

func ContainerByDig() *dig.Container {
    d := dig.New()
    _ = d.Provide(dao.NewOrderDao)
    _ = d.Provide(server.NewOrderServer)
    _ = d.Provide(controller.NewOrderHandler)
    return d
}

First through dig.New() creates a di container. The Provide function is used to add service providers, and the first parameter of the Provide function is essentially a function. One tells the container "What can I offer and what do I need to provide it?" A function of.

Let's look at the second server, for example. NewOrderServer,

package server

import (
    "github.com/wuqinqiang/digvswire/dao"
)

var _ OrderServerInterface = &OrderServer{}

type OrderServerInterface interface {
    GetUserOrderList(userId string) ([]dao.Order, error)
}

type OrderServer struct {
    orderDao dao.OrderDao
}

func NewOrderServer(order dao.OrderDao) OrderServerInterface {
    return &OrderServer{orderDao: order}
}

func (o *OrderServer) GetUserOrderList(userId string) (orderList []dao.Order, err error) {
    return o.orderDao.GetOrderListById(userId)
}

Here, NewOrderServer(xxx) in Provide means "I can provide an OrderServerInterface service, but I need to rely on a dao.OrderDao".

In the code just now,

_ = d.Provide(dao.NewOrderDao)
_ = d.Provide(server.NewOrderServer)
_ = d.Provide(controller.NewOrderHandler)

Because our call chain is controller->server->dao, their dependency is essentially controller<-server<-dao, which is not a concrete implementation but an abstract interface.

So you see that providers are written in dependency order.

This is not necessary at all, because dig will only analyze these functions, extract their parameters and return values. The container structure is then organized according to the parameters returned. The incoming function is not executed at this step, so the order before and after the Provide phase is not important as long as you make sure you don't miss the dependencies.

With everything in place, we started registering a route to get orders.

err := router.RegisterRouter(e, d)

// router.go
func RegisterRouter(e *gin.Engine, dig *dig.Container) error {
    return dig.Invoke(func(handle *controller.OrderHandler) {
        e.GET("/user/orders", handle.GetUserOrderList)
    })
}

At this point, invoke is what you really need to get the *controller.OrderHandler object.

Calling the invoke method will parse the incoming parameter, with handle *controller in it. OrderHandler, it will go to the container to find which Provide r came in whose function return type is handle *controller.OrderHandler,

Can be found accordingly,

_ = d.Provide(controller.NewOrderHandler)
// Corresponding
func NewOrderHandler(server server.OrderServerInterface) *OrderHandler {
    return &OrderHandler{
        server: server,
    }
}

The function was found to have a formal parameter server.OrderServerInterface, then find the corresponding function that returns this type,

_ = d.Provide(server.NewOrderServer)
//Corresponding
func NewOrderServer(order dao.OrderDao) OrderServerInterface {
    return &OrderServer{orderDao: order}
}

The formal parameter (order dao.OrderDao) was also found.

_ = d.Provide(dao.NewOrderDao)
//Corresponding
func NewOrderDao() OrderDao {
    return new(OrderDaoImpl)
}

Finally, you find that NewOrderDao has no dependencies and no longer needs to query dependencies. Start executing the function call NewOrderDao(), pass the returned OrderDao into the upper NewOrderServer(order dao.OrderDao) to make the function call, and the OrderServerInterface returned by NewOrderServer(order dao.OrderDao) continues to return to the upper NewOrderHandler (server.OrderServerInterface) to perform the call, Finally, pass the *OrderHandler returned from the function call to dig.Invoke(func(handle *controller.OrderHandler) {},

The whole link is ready. Describe the process in a simple picture

The entire dig process uses a reflection mechanism to compute dependencies and construct dependent objects at run time.

What can be the problem?

Suppose I comment out a line of code for the Provide now, for example,

func ContainerByDig() *dig.Container {
    d := dig.New()
    //_ = d.Provide(dao.NewOrderDao)
    _ = d.Provide(server.NewOrderServer)
    _ = d.Provide(controller.NewOrderHandler)
    return d
}

We don't report any errors when compiling the project, we only find the missing dependencies at run time.

wire

Or the code above, we use wire s as our DI container.

wire also has two core concepts: Provider and Injector.

The concept of Provider is the same as that of dig:'What can I offer? What do I need to depend on?

Like the following wire. Code in go,

//+build wireinject

package wire

import (
    "github.com/google/wire"
    "github.com/wuqinqiang/digvswire/controller"
    "github.com/wuqinqiang/digvswire/dao"
    "github.com/wuqinqiang/digvswire/server"
)

var orderSet = wire.NewSet(
    dao.NewOrderDao,
    server.NewOrderServer,
    controller.NewOrderHandler)

func ContainerByWire() *controller.OrderHandler {
    wire.Build(orderSet)
    return &controller.OrderHandler{}
}

Where dao.NewOrderDao, server.NewOrderServer and controller.NewOrderHandler is the Provider.

You'll find that wire is also called here. NewSet puts them together and assigns them to a variable, orderSet.

The concept of a Provider Set is actually used. The idea is to package a set of related providers.

The benefits are:

  • Structural dependency is clear and easy to read.
  • Reduce Build in injector as a group.

As for injector s, it is essentially a function that calls a Provider based on a dependency, then ultimately generates the object (service) we want.

For example, the ContainerByWire() above is an injector.

Then wire. The overall idea of the go file is to define the injector and then implement the required Provider.

Finally at the current wire. After executing the wires command under the go folder,

If you have a problem with your dependency at this point, you will get an error alert. For example, I'm hiding the Dao above. NewOrderDao, then it appears

If the dependency does not have a problem, a wire_is generated Gen.go file.

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//+build !wireinject

package wire

import (
    "github.com/google/wire"
    "github.com/wuqinqiang/digvswire/controller"
    "github.com/wuqinqiang/digvswire/dao"
    "github.com/wuqinqiang/digvswire/server"
)

// Injectors from wire.go:

func ContainerByWire() *controller.OrderHandler {
    orderDao := dao.NewOrderDao()
    orderServerInterface := server.NewOrderServer(orderDao)
    orderHandler := controller.NewOrderHandler(orderServerInterface)
    return orderHandler
}

// wire.go:

var orderSet = wire.NewSet(dao.NewOrderDao, server.NewOrderServer, controller.NewOrderHandler)

You need to pay attention to the above two files. We see wires. The first line in go//+build wireinject, this build tag ensures that wires are ignored during regular compilation. Go file. The opposite wire_ //+build in gen.go! Wireinject. The two opposing build tags are designed to ensure that in any case, only one of the two files is valid and avoid compilation errors where the ContainerByWire() method is redefined.

Now that we can actually use injector, we'll replace dig in the entry file.

func serverStart() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("init app err:%v\n", err)
        }
    }()
    e := gin.Default()
    err := router.RegisterRouterByWire(e, wire.ContainerByWire())
    if err != nil {
        panic(err)
    }
    _ = e.Run(":8090")
}
func RegisterRouterByWire(e *gin.Engine, handler *controller.OrderHandler) error {
    e.GET("/v2/user/orders", handler.GetUserOrderList)
    return nil
}

Business as usual.

Of course wires have a point to note, on wires. The first few lines in the go file:

//+build wireinject

package wire

There is a blank line between build tag and package. If there is no blank line, build tag cannot recognize it, and a duplicate declaration error will be reported when compiling:

There are many advanced operations you can learn about yourself.

summary

The above outlines the two DI tools, dig and wire, in go. Where dig is a dependency injection through runtime reflection. Wire, on the other hand, generates corresponding dependency injection code by command based on custom code, completing dependency injection at compile time without reflection mechanism. The benefits are:

  • For ease of investigation, if there are dependency errors, they can be found at compile time. dig can only discover dependency errors at run time.
  • To avoid dependency expansion, wire s generate code that only contains dependencies, and dig may have many useless dependencies.
  • Dependency static source exists for tool analysis.

Reference

[1] github.com/google/wire

[2] github.com/uber-go/dig

[3] medium.com/@dche423/master-wire-cn...

[4] www.cnblogs.com/li-peng/p/14708132...

Topics: Go