開篇
dig
和 wire
都是 Go
依賴注入的工具,那麼,本質上功能相似的框架,為什麼要從 dig
切換成 wire
?
場景
我們從場景出發。
假設我們的專案分層是:router->controller->service->dao
。
大概就長這樣:
現在我們需要對外暴露一個訂單服務的介面。
首頁看 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) {}
,
整個鏈路就通了。用一個簡陋的圖來描述這個過程
dig
的整個流程採用的是反射機制,在執行時計算依賴關係,構造依賴物件。
這樣會存在什麼問題?
假設我現在註釋掉 Provide
的一行程式碼,比如,
func ContainerByDig() *dig.Container {
d := dig.New()
//_ = d.Provide(dao.NewOrderDao)
_ = d.Provide(server.NewOrderServer)
_ = d.Provide(controller.NewOrderHandler)
return d
}
我們在編譯專案的時候並不會報任何錯誤,只會在執行時才發現缺少了依賴項。
wire
還是上面的程式碼,我們使用 wire
作為我們的 DI
容器。
wire
也有兩個核心概念: Provider
和 Injector
。
其中 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.NewOrderDao
、server.NewOrderServer
以及 controller.NewOrderHandler
就是 Provider
。
你會發現這裡還呼叫 wire.NewSet
把他們整合在一起,賦值給了一個變數 orderSet
。
其實是用到 ProviderSet
的概念。原理就是把一組相關的 Provider
進行打包。
這樣的好處是:
- 結構依賴清晰,便於閱讀。
- 以組的形式,減少
injector
裡的Build
。
至於 injector
,本質上就是按照依賴關係呼叫 Provider
的函式,然後最終生成我們想要的物件(服務)。
比如上面的 ContainerByWire()
就是一個 injector
。
那麼 wire.go
檔案整體的思路就是:定義好 injector
,然後實現所需的 Provider
。
最後在當前 wire.go
資料夾下執行 wire
命令後,
此時如果你的依賴項存在問題,那麼就會報錯提示。比如我現在隱藏上面的 dao.NewOrderDao
,那麼會出現
如果依賴不存在問題,最終會生成一個 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 tag
和 package
他們之間是有空行的,如果沒有空行,build tag
識別不了,那麼編譯的時候就會報重複宣告的錯誤:
還有很多高階的操作可以自行了解。
總結
以上大體介紹了 go 中 dig
和 wire
兩個 DI
工具。其中 dig
是通過執行時反射實現的依賴注入。 而 wire
是根據自定義的程式碼,通過命令,生成相應的依賴注入程式碼,在編譯期就完成依賴注入,無需反射機制。 這樣的好處是:
- 方便排查,如果存在依賴錯誤,編譯時就能發現。而
dig
只能在執行時才能發現依賴錯誤。 - 避免依賴膨脹,
wire
生成的程式碼只包含被依賴的,而dig
可能會存在好多無用依賴。 - 依賴關係靜態存在原始碼,便於工具分析。
Reference
[3] medium.com/@dche423/master-wire-cn...
[4] www.cnblogs.com/li-peng/p/14708132...
本作品採用《CC 協議》,轉載必須註明作者和本文連結