寫在前面 在Java日常開發過程中,實現Excel文件的導入導出功能是一項常見的需求。 通過使用相關的Java庫,如Apache POI、EasyPoi或EasyExcel,可以輕鬆地實現Excel文件的讀寫操作。 而這篇文章將介紹如何在Java中使用Apache POI、EasyPoi 和Easy ...
控制反轉和依賴註入
控制反轉(Inversion of Control,縮寫為IoC),是面向對象編程中的一種設計原則,可以用來減低電腦代碼之間的耦合度。其中最常見的方式叫做依賴註入(Dependency Injection,簡稱DI)。依賴註入是生成靈活和鬆散耦合代碼的標準技術,通過明確地向組件提供它們所需要的所有依賴關係。在 Go 中通常採用將依賴項作為參數傳遞給構造函數的形式:
構造函數NewBookRepo
在創建BookRepo
時需要從外部將依賴項db
作為參數傳入,我們在NewBookRepo
中無需關註db
的創建邏輯,實現了代碼解耦。
// NewBookRepo 創建BookRepo的構造函數
func NewBookRepo(db *gorm.DB) *BookRepo {
return &BookRepo{db: db}
}
區別於控制反轉,如果在NewBookRepo
函數中自行創建相關依賴,這將導致代碼高度耦合併且難以維護和調試。
// NewBookRepo 創建BookRepo的構造函數
func NewBookRepo() *BookRepo {
db, _ := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
return &BookRepo{db: db}
}
為什麼需要依賴註入工具
現在我們已經知道了應該在開發中儘可能地使用控制反轉和依賴註入將程式解耦開來,從而寫出靈活和易測試的程式。
在小型應用程式中,我們可以自行創建依賴並手動註入。但是在一個大型應用程式中,手動去實現所有依賴的創建和註入就會比較繁瑣。
例如,在一些常見的HTTP服務中,會根據業務需要劃分出不同的代碼層:
├── internal
│ ├── conf
│ │ └── conf.go
│ ├── data
│ │ └── data.go
│ ├── server
│ │ └── server.go
│ └── service
│ └── service.go
└── main.go
我們的服務需要有一個配置,指定工作模式、連接的資料庫和監聽埠等信息。
// conf/conf.go
// NewDefaultConfig 返回預設配置,不需要依賴
func NewDefaultConfig() *Config {...}
我們這裡定義了一個預設配置,當然後續可以支持從配置文件或環境變數讀取配置信息。
在程式的data
層,需要定義一個連接資料庫的函數,它依賴上面定義的Config
並返回一個*gorm.DB
(這裡使用gorm連接資料庫)。
// data/data.go
// NewDB 返回資料庫連接對象
func NewDB(cfg *conf.Config) (*gorm.DB, error) {...}
同時定義一個BookRepo
,它有一些數據操作相關的方法。它的構造函數NewBookRepo
依賴*gorm.DB
,並返回一個*BookRepo
。
// data/data.go
type BookRepo struct {
db *gorm.DB
}
func NewBookRepo(db *gorm.DB) *BookRepo {...}
Service
層位於data
層和Server
層的中間,它負責實現對外服務。其中構造函數 NewBookService
依賴Config
和BookRepo
。
// service/service.go
type BookService struct {
config *conf.Config
repo *data.BookRepo
}
func NewBookService(cfg *conf.Config, repo *data.BookRepo) *BookService {...}
server
層又有一個NewServer
構造函數,它依賴外部傳入Config
和BookService
。
// server/server.go
type Server struct {
config *conf.Config
service *service.BookService
}
func NewServer(cfg *conf.Config, srv *service.BookService) *Server {...}
在main.go
文件中又依賴Server
創建一個app
。
// main.go
type Server interface {
Run()
}
type App struct {
server Server
}
func newApp(server Server) *App {...}
由於在程式中定義了大量需要依賴註入的構造函數,程式的main
函數中會出現以下情形。所有依賴的創建和順序都需要手動維護。
// main.go
func main() {
cfg := conf.NewDefaultConfig()
db, _ := data.NewDB(cfg)
repo := data.NewBookRepo(db)
bookSrv := service.NewBookService(cfg, repo)
server := server.NewServer(cfg, bookSrv)
app := newApp(server)
app.Run()
}
我們確實需要一個工具來解決這類問題。
Wire
Wire
是一個專為依賴註入(Dependency Injection
)設計的代碼生成工具,它可以自動生成用於初始化各種依賴關係的代碼,從而幫助我們更輕鬆地管理和註入依賴關係。
Wire 安裝
我們可以執行以下命令來安裝 Wire
工具:
$ go install github.com/google/wire/cmd/wire@latest
安裝之前請確保已將 $GOPATH/bin
添加到環境變數 $PATH
里。
Wire 的基本使用
前置代碼準備
雖然我們在前面已經通過 go install
命令安裝了 Wire
命令行工具,但在具體項目中,我們仍然需要通過以下命令安裝項目所需的 Wire
依賴,以便結合 Wire
工具生成代碼:
$ go get github.com/google/wire@latest
接下來,讓我們模擬一個簡單的 web
博客項目,編寫查詢文章介面的相關代碼,並使用 Wire
工具生成代碼。
項目的目錄結構如下:
.
├── ioc
│ └── article.go
├── main.go
├── service
│ └── article.go
├── web
│ └── article.go
└── wire.go
首先,我們先定義相關類型與方法,並提供對應的 初始化函數:
- 定義
PostHandler
結構體,創建註冊路由的方法RegisterRoutes
和查詢文章路由處理的方法GetPostById
以及初始化的函數NewPostHandler
,並且它依賴於IPostService
介面:
type PostHandler struct {
serv service.IPostService
}
func (h *PostHandler) RegisterRoutes(engine *gin.Engine) {
engine.GET("/post/:id", h.GetPostById)
}
func (h *PostHandler) GetPostById(ctx *gin.Context) {
content := h.serv.GetPostById(ctx, ctx.Param("id"))
ctx.String(http.StatusOK, content)
}
func NewPostHandler(serv service.IPostService) *PostHandler {
return &PostHandler{serv: serv}
}
- 定義
IPostService
介面,並提供了一個具體實現PostService
,接著創建GetPostById
方法,用於處理查詢文章的邏輯,然後提供初始化函數NewPostService
,該函數返回IPostService
介面類型:
type IPostService interface {
GetPostById(ctx context.Context, id string) string
}
var _ IPostService = (*PostService)(nil)
type PostService struct {
}
func (s *PostService) GetPostById(ctx context.Context, id string) string {
return "歡迎訪問博客"
}
func NewPostService() IPostService {
return &PostService{}
}
- 定義一個初始化
gin.Engine
函數NewGinEngineAndRegisterRoute
,該函數依賴於*handler.PostHandler
類型,函數內部調用相關handler
結構體的方法創建路由:
func NewGinEngineAndRegisterRoute(postHandler *web.PostHandler) *gin.Engine {
engine := gin.Default()
postHandler.RegisterRoutes(engine)
return engine
}
使用 Wire 工具生成代碼
前置代碼已經準備好了,接下來我們編寫核心代碼,以便 Wire
工具能生成相應的依賴註入代碼。
- 首先我們需要創建一個
wire
的配置文件,通常命名為wire.go
。在這個文件里,我們需要定義一個或者多個註入器函數(Injector
函數,接下來的內容會對其進行解釋),以便指引Wire
工具生成代碼。
func InitializeApp() *gin.Engine {
wire.Build(
web.NewPostHandler,
service.NewPostService,
ioc.NewGinEngineAndRegisterRoute,
)
return &gin.Engine{}
}
在上述代碼中,我們定義了一個用於初始化 gin.Engine
的註入器函數,在該函數內部,我們使用了 wire.Build
方法來聲明依賴關係,其中包括 PostHandler
、PostService
和 InitGinEngine
作為依賴的構造函數。
wire.Build
的作用是 連接或綁定我們之前定義的所有初始化函數。當我們運行 wire
工具來生成代碼時,它就會根據這些依賴關係來自動創建和註入所需的實例。
註意:文件首行必須加上 //go:build wireinject
或 // +build wireinject
(go 1.18
之前的版本使用) 註釋,作用是只有在使用 wire
工具時才會編譯這部分代碼,其他情況下忽略。
- 接下來在
wire.go
文件所處目錄下執行wire
命令,生成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 (
"github.com/gin-gonic/gin"
"golang-example/wire/blog/ioc"
"golang-example/wire/blog/service"
"golang-example/wire/blog/web"
)
// Injectors from wire.go:
func InitializeApp() *gin.Engine {
iPostService := service.NewPostService()
postHandler := web.NewPostHandler(iPostService)
engine := ioc.NewGinEngineAndRegisterRoute(postHandler)
return engine
}
生成的代碼和我們手寫區別不大,當我們的組件很多,依賴關係複雜的時候,我們才會感覺到 Wire
工具的好處。
Wire 的核心概念
Wire
有兩個核心概念:提供者(providers
)和註入器(injectors
)。
Wire 提供者(providers)
提供者:一個可以產生值的函數,也就是有返回值的函數。例如入門代碼里的 NewPostHandler
函數:
func NewPostHandler(serv service.IPostService) *PostHandler {
return &PostHandler{serv: serv}
}
返回值不僅限於一個,如果有需要的話,可以額外添加一個 error
的返回值。
如果提供者過多的時候,我們還可以以分組的形式進行連接,例如將 post
相關的 handler
和 service
進行組合:
package web
var PostSet = wire.NewSet(NewPostHandler, service.NewPostService)
使用 wire.NewSet
函數將提供者進行分組,該函數返回一個 ProviderSet
結構體。不僅如此,wire.NewSet
還能對多個 ProviderSet
進行分組 wire.NewSet(PostSet, XxxSet)
。
對於之前的 InitializeApp
函數,我們可以這樣升級:
func InitializeAppV2() *gin.Engine {
wire.Build(
web.PostSet,
ioc.NewGinEngineAndRegisterRoute,
)
return &gin.Engine{}
}
然後通過 Wire
命令生成代碼,和之前的結果一致。
Wire 註入器(injectors)
註入器(injectors
)的作用是將所有的提供者(providers
)連接起來,回顧一下我們之前的代碼:
func InitializeApp() *gin.Engine {
wire.Build(
web.NewPostHandler,
service.NewPostService,
ioc.NewGinEngineAndRegisterRoute,
)
return &gin.Engine{}
}
InitializeApp
函數就是一個註入器,函數內部通過 wire.Build
函數連接所有的提供者,然後返回 &gin.Engine{}
,該返回值實際上並沒有使用到,只是為了滿足編譯器的要求,避免報錯而已,真正的返回值來自 ioc.NewGinEngineAndRegisterRoute
。
Wire 高級應用
綁定介面
回顧我們之前編寫的代碼:
package web
···
func NewPostHandler(serv service.IPostService) *PostHandler {
return &PostHandler{serv: serv}
}
···
pakacge service
···
func NewPostService() IPostService {
return &PostService{}
}
···
NewPostHandler
函數依賴於 service.IPostService
介面,NewPostService
函數返回的是 IPostService
介面的值,這兩個地方的類型匹配,因此 Wire
工具能夠正確識別並生成代碼。然而,這並不是推薦的最佳實踐。因為在 Go
中的 最佳實踐 是返回 具體的類型 的值,所以最好讓 NewPostService
返回具體類型 PostService
的值:
func NewPostService() *PostService {
return &PostService{}
}
但是這樣,Wire
工具將認為 IPostService
介面類型與 PostService
類型不匹配,導致生成代碼失敗。因此我們需要修改註入器的代碼:
func InitializeApp() *gin.Engine {
wire.Build(
web.NewPostHandler,
service.NewPostService,
ioc.NewGinEngineAndRegisterRoute,
wire.Bind(new(service.IPostService), new(*service.PostService)),
)
return &gin.Engine{}
}
使用 wire.Bind
來建立介面類型和具體的實現類型之間的綁定關係,這樣 Wire
工具就可以根據這個綁定關係進行類型匹配並生成代碼。
wire.Bind
函數的第一個參數是指向所需介面類型值的指針,第二個實參是指向實現該介面的類型值的指針。
結構體提供者(Struct Providers)
Wire
庫有一個函數是 wire.Struct
,它能根據現有的類型進行構造結構體,我們來看看下麵的例子:
package main
type Name string
func NewName() Name {
return"Jack"
}
type PublicAccount string
func NewPublicAccount() PublicAccount {
return"Hello World"
}
type User struct {
MyName Name
MyPublicAccount PublicAccount
}
func InitializeUser() *User {
wire.Build(
NewName,
NewPublicAccount,
wire.Struct(new(User), "MyName", "MyPublicAccount"),
)
return &User{}
}
上述代碼中,首先定義了自定義類型 Name
和 PublicAccount
以及結構體類型 User
,並分別提供了 Name
和 PublicAccount
的初始化函數(providers
)。然後定義一個註入器(injectors
)InitializeUser
,用於構造連接提供者並構造 *User
實例。
使用 wire.Struct
函數需要傳遞兩個參數,第一個參數是結構體類型的指針值,另一個參數是一個可變參數,表示需要註入的結構體欄位的名稱集。
根據上述代碼,使用 Wire
工具生成的代碼如下所示:
func InitializeUser() *User {
name := NewName()
publicAccount := NewPublicAccount()
user := &User{
MyName: name,
MyPublicAccount: publicAccount,
}
return user
}
如果我們不想返回指針類型,只需要修改 InitializeUser
函數的返回值為非指針即可。
綁定值
有時候,我們可以在註入器中通過 值表達式 給一個類型進行賦值,而不是依賴提供者(providers
)。
func InjectUser() User {
wire.Build(wire.Value(User{MyName: "Jack"}))
return User{}
}
在上述代碼中,使用 wire.Value
函數通過表達式直接指定 MyName
的值,生成的代碼如下所示:
func InjectUser() User {
user := _wireUserValue
return user
}
var (
_wireUserValue = User{MyName: "Jack"}
)
需要註意的是,值表達式將被覆制到生成的代碼文件中。
對於介面類型,可以使用 InterfaceValue
:
func InjectPostService() service.IPostService {
wire.Build(wire.InterfaceValue(new(service.IPostService), &service.PostService{}))
return nil
}
使用結構體欄位作為提供者(providers)
有些時候,你可以使用結構體的某個欄位作為提供者,從而生成一個類似 GetXXX
的函數。
func GetUserName() Name {
wire.Build(
NewUser,
wire.FieldsOf(new(User), "MyName"),
)
return ""
}
你可以使用 wire.FieldsOf
函數添加任意欄位,生成的代碼如下所示:
func GetUserName() Name {
user := NewUser()
name := user.MyName
return name
}
func NewUser() User {
return User{MyName: Name("Jack"), MyPublicAccount: PublicAccount("HelloWorld")}
}
清理函數
如果一個提供者創建了一個需要清理的值(例如關閉一個文件),那麼它可以返回一個閉包來清理資源。註入器會用它來給調用者返回一個聚合的清理函數,或者在註入器實現中稍後調用的提供商返回錯誤時清理資源。
並且 Wire
對 Provider
的返回值個數及順序有以下限制:
- 第一個返回值是需要生成的對象
- 如果有 2 個返回值,第二個返回值必須是 func() 或 error
- 如果有 3 個返回值,第二個返回值必須是 func(),而第三個返回值必須是
// db.go
func InitGormDB()(*gorm.DB, func(), error) {
// 初始化db鏈接
// ...
cleanFunc := func(){
db.Close()
}
return db, cleanFunc, nil
}
// wire.go
func BuildInjector() (*Injector, func(), error) {
wire.Build(
common.InitGormDB,
// ...
NewInjector
)
return new(Injector), nil, nil
}
// 生成的wire_gen.go
func BuildInjector() (*Injector, func(), error) {
db, cleanup, err := common.InitGormDB()
// ...
return injector, func(){
// 所有provider的清理函數都會在這裡
cleanup()
}, nil
}
// main.go
injector, cleanFunc, err := app.BuildInjector()
defer cleanFunc()
備用註入器語法
如果你不喜歡將類似這種寫法 → return &gin.Engine{}
放在你的註入器函數聲明的末尾,你可以用 panic
來更簡潔地寫它:
func InitializeGin() *gin.Engine {
panic(wire.Build(/* ... */))
}
總結
在本文中,我們詳細探討了 Go Wire
工具的基本用法和高級特性。它是一個專為依賴註入設計的代碼生成工具,它不僅提供了基礎的依賴解析和代碼生成功能,還支持多種高級用法,如介面綁定和構造結構體。
依賴註入的設計模式應用非常廣泛,Wire
工具讓依賴註入在 Go
語言中變得更簡單。
本文的所有代碼在這裡。