為什麼把 dig 遷移到 wire

Remember發表於2021-08-02

開篇

digwire 都是 Go 依賴注入的工具,那麼,本質上功能相似的框架,為什麼要從 dig 切換成 wire

場景

我們從場景出發。

假設我們的專案分層是:router->controller->service->dao

大概就長這樣:

image

現在我們需要對外暴露一個訂單服務的介面。

首頁看 main.go 檔案。

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")
}

這裡使用了 gin 啟動專案。 然後我們檢視 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
}

首先通過 dig.New() 建立一個 di 容器。 Provide 函式用於新增服務提供者, Provide 函式第一個引數本質上是一個函式。一個告訴容器 “我能提供什麼,為了提供它,我需要什麼?” 的函式。

比如我們看第二個 server.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)
}

這裡的 NewOrderServer(xxx)Provide 中的語意就是 “我能提供一個 OrderServerInterface 服務,但是我需要依賴一個 dao.OrderDao“。

剛才的程式碼中,

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

因為我們的呼叫鏈是 controller->server->dao,那麼本質上他們的依賴是 controller<-server<-dao,只是依賴的不是具體的實現,而是抽象的介面。

所以你看到 Provide 是按照依賴關係順序寫的。

其實完全沒有必要,因為這一步 dig 只會對這些函式進行分析,提取函式的形參以及返回值。然後根據返回的引數來組織容器結構。 並不會在這一步執行傳入的函式,所以在 Provide 階段前後順序並不重要,只要確保不遺漏依賴項即可。

萬事俱備,我們開始註冊一個能獲取訂單的路由,

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)
    })
}

此時,呼叫 invoke, 才是真正需要獲取 *controller.OrderHandler 物件。

呼叫 invoke 方法,會對傳入的引數做分析,引數中存在 handle *controller.OrderHandler, 就會去容器中尋找哪個 Provide 進來的函式返回型別是 handle *controller.OrderHandler,

就能對應找到,

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

發現這個函式有形參 server.OrderServerInterface,那就去找對應返回此型別的函式,

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

又發現形參 (order dao.OrderDao),

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

最後發現 NewOrderDao 沒有依賴,不需要再查詢依賴。開始執行函式的呼叫 NewOrderDao(),把返回的 OrderDao 傳入到上層的 NewOrderServer(order dao.OrderDao) 進行函式呼叫, NewOrderServer(order dao.OrderDao) 返回的 OrderServerInterface 繼續返回到上層 NewOrderHandler(server server.OrderServerInterface) 執行呼叫,最後再把函式呼叫返回的 *OrderHandler 傳遞給 dig.Invoke(func(handle *controller.OrderHandler) {},

整個鏈路就通了。用一個簡陋的圖來描述這個過程 image

dig 的整個流程採用的是反射機制,在執行時計算依賴關係,構造依賴物件。

這樣會存在什麼問題?

假設我現在註釋掉 Provide 的一行程式碼,比如,

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

我們在編譯專案的時候並不會報任何錯誤,只會在執行時才發現缺少了依賴項。 image

wire

還是上面的程式碼,我們使用 wire 作為我們的 DI 容器。

wire 也有兩個核心概念: ProviderInjector

其中 Provider 的概念和 dig 的概念是一樣的:”我能提供什麼?我需要什麼依賴”。

比如下面 wire.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{}
}

其中,dao.NewOrderDaoserver.NewOrderServer 以及 controller.NewOrderHandler 就是 Provider

你會發現這裡還呼叫 wire.NewSet 把他們整合在一起,賦值給了一個變數 orderSet

其實是用到 ProviderSet 的概念。原理就是把一組相關的 Provider 進行打包。

這樣的好處是:

  • 結構依賴清晰,便於閱讀。
  • 以組的形式,減少 injector 裡的 Build

至於 injector,本質上就是按照依賴關係呼叫 Provider 的函式,然後最終生成我們想要的物件(服務)。

比如上面的 ContainerByWire() 就是一個 injector

那麼 wire.go 檔案整體的思路就是:定義好 injector,然後實現所需的 Provider

最後在當前 wire.go 資料夾下執行 wire 命令後,

此時如果你的依賴項存在問題,那麼就會報錯提示。比如我現在隱藏上面的 dao.NewOrderDao,那麼會出現 image

如果依賴不存在問題,最終會生成一個 wire_gen.go 檔案。

// 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)

需要注意上面兩個檔案。我們看到 wire.go 中第一行 //+build wireinject ,這個 build tag 確保在常規編譯時忽略 wire.go 檔案。 而與之相對的 wire_gen.go 中的 //+build !wireinject。 兩個對立的 build tag 是為了確保在任意情況下,兩個檔案只有一個檔案生效, 避免出現 “ContainerByWire() 方法被重新定義” 的編譯錯誤。

現在我們可以真正使用 injector 了,我們在入口檔案中替換成 dig

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
}

一切正常。

當然 wire 有一個點需要注意,在 wire.go 檔案中開頭幾行:

//+build wireinject

package wire

build tagpackage 他們之間是有空行的,如果沒有空行,build tag 識別不了,那麼編譯的時候就會報重複宣告的錯誤: image

還有很多高階的操作可以自行了解。

總結

以上大體介紹了 go 中 digwire 兩個 DI 工具。其中 dig 是通過執行時反射實現的依賴注入。 而 wire 是根據自定義的程式碼,通過命令,生成相應的依賴注入程式碼,在編譯期就完成依賴注入,無需反射機制。 這樣的好處是:

  • 方便排查,如果存在依賴錯誤,編譯時就能發現。而 dig 只能在執行時才能發現依賴錯誤。
  • 避免依賴膨脹,wire 生成的程式碼只包含被依賴的,而 dig 可能會存在好多無用依賴。
  • 依賴關係靜態存在原始碼,便於工具分析。

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

本作品採用《CC 協議》,轉載必須註明作者和本文連結
吳親庫裡

相關文章