1. 程式人生 > >從別人的程式碼中學習golang系列--02

從別人的程式碼中學習golang系列--02

這篇部落格還是整理從https://github.com/LyricTian/gin-admin 這個專案中學習的golang相關知識 作者在專案中使用了https://github.com/google/wire 做依賴注入,這個庫我之前沒有使用過,看了作者程式碼中的使用,至少剛開始是看著優點懵,不知道是做什麼,所以這篇部落格主要就是整理這個包的使用 ## 依賴注入是什麼? 如果你搜索**依賴注入**,百度百科裡可能先看到的是**控制反轉**,下面是百度百科的解釋 > **控制反轉**(Inversion of Control,縮寫為**IoC**),是[面向物件程式設計](https://baike.baidu.com/item/面向物件程式設計)中的一種設計原則,可以用來減低計算機程式碼之間的[耦合度](https://baike.baidu.com/item/耦合度)。其中最常見的方式叫做**依賴注入**(Dependency Injection,簡稱**DI**),還有一種方式叫“依賴查詢”(Dependency Lookup)。通過控制反轉,物件在被建立的時候,由一個調控系統內所有物件的外界實體將其所依賴的物件的引用傳遞給它。也可以說,依賴被注入到物件中。 這樣的解釋可能還是不好理解,所以我們通過一個簡單的程式碼來理解應該就清楚很多。 我們用程式實現:小明對世界說:"hello golang" 這裡將小明抽象為People 說的內容抽象為: Message 小明說 hello golang 抽象為:Event, 程式碼如下: ```go package main import "fmt" var msg = "Hello World!" func NewMessage() Message { return Message(msg) } // 要說的內容的抽象 type Message string func NewPeople(m Message) People { return People{name: "小明", message: m} } // 小明這個人的抽象 type People struct { name string message Message } // 小明這個人會說話 func (p People) SayHello() string { msg := fmt.Sprintf("%s 對世界說:%s\n", p.name, p.message) return msg } func NewEvent(p People) Event { return Event{people: p} } // 小明去說話這個行為抽象為一個事件 type Event struct { people People } func (e Event) start() { msg := e.people.SayHello() fmt.Println(msg) } func main() { message := NewMessage() people := NewPeople(message) event := NewEvent(people) event.start() } ``` 從上面這個程式碼我們可以看出,我們必須先初始化一個`NewMessage`, 因為`NewPeople `依賴它,`NewEvent` 依賴`NewPeople`. 這還是一種比較簡單的依賴關係,實際生產的依賴關係可能會更復雜,那麼什麼好的辦法來處理這種依賴,https://github.com/google/wire 就是來幹這件事情的。 ## wire依賴注入例子 ### 栗子1 安裝:` go get github.com/google/wire/cmd/wire` 上面的程式碼,我們用wire的方式實現,程式碼如下: ```go package main import ( "fmt" "github.com/google/wire" ) var msg = "Hello World!" func NewMessage() Message { return Message(msg) } // 要說的內容的抽象 type Message string func NewPeople(m Message) People { return People{name: "小明", message: m} } // 小明這個人的抽象 type People struct { name string message Message } // 小明這個人會說話 func (p People) SayHello() string { msg := fmt.Sprintf("%s 對世界說:%s\n", p.name, p.message) return msg } func NewEvent(p People) Event { return Event{people: p} } // 小明去說話這個行為抽象為一個事件 type Event struct { people People } func (e Event) start() { msg := e.people.SayHello() fmt.Println(msg) } func InitializeEvent() Event { wire.Build(NewEvent, NewPeople, NewMessage) return Event{} } func main() { e := InitializeEvent() e.start() } ``` 這裡我們不用再手動初始化`NewEvent, NewPeople, NewMessage`,而是通過需要初始化的函式傳遞給`wire.Build` , 這三者的依賴關係,wire 會幫我們處理,我們通過wire . 的方式生成程式碼: ```bash ➜ useWireBaseExample2 wire . wire: awesomeProject/202006/useWireBaseExample2: wrote /home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample2/wire_gen.go ➜ useWireBaseExample2 ``` 會在當前目錄下生成wire_gen.go的程式碼,內容如下: ```go // Code generated by Wire. DO NOT EDIT. //go:generate wire //+build !wireinject package main import ( "fmt" ) // Injectors from main.go: func InitializeEvent() Event { message := NewMessage() people := NewPeople(message) event := NewEvent(people) return event } // main.go: var msg = "Hello World!" func NewMessage() Message { return Message(msg) } // 要說的內容的抽象 type Message string func NewPeople(m Message) People { return People{name: "小明", message: m} } // 小明這個人的抽象 type People struct { name string message Message } // 小明這個人會說話 func (p People) SayHello() string { msg2 := fmt.Sprintf("%s 對世界說:%s\n", p.name, p.message) return msg2 } func NewEvent(p People) Event { return Event{people: p} } // 小明去說話這個行為抽象為一個事件 type Event struct { people People } func (e Event) start() { msg2 := e.people.SayHello() fmt.Println(msg2) } func main() { e := InitializeEvent() e.start() } ``` 程式碼中wire為我們生成了如下程式碼: ```go // Injectors from main.go: func InitializeEvent() Event { message := NewMessage() people := NewPeople(message) event := NewEvent(people) return event } ``` 在看看我們剛開始寫的程式碼,發現其實是一樣的,是不是感覺方便了很多。 **注意:當使用 Wire 時,我們將同時提交 Wire.go 和 Wire _ gen 到程式碼倉庫** wire 能做的事情很多,如果我們相互依賴的初始化其中有初始化失敗的,wire也能幫我們很好的處理。 ### 栗子2 ```go package main import ( "errors" "fmt" "os" "time" "github.com/google/wire" ) var msg = "Hello World!" func NewMessage() Message { return Message(msg) } // 要說的內容的抽象 type Message string func NewPeople(m Message) People { var grumpy bool if time.Now().Unix()%2 == 0 { grumpy = true } return People{name: "小明", message: m, grumpy: grumpy} } // 小明這個人的抽象 type People struct { name string message Message grumpy bool // 脾氣是否暴躁 } // 小明這個人會說話 func (p People) SayHello() string { if p.grumpy { // 脾氣暴躁,心情不好 msg := "Go away !" return msg } msg := fmt.Sprintf("%s 對世界說:%s\n", p.name, p.message) return msg } func NewEvent(p People) (Event, error) { if p.grumpy { return Event{}, errors.New("could not create event: event greeter is grumpy") } return Event{people: p}, nil } https://github.com/LyricTian/gin-admin // 小明去說話這個行為抽象為一個事件 type Event struct { people People } func (e Event) start() { msg := e.people.SayHello() fmt.Println(msg) } func InitializeEvent() (Event, error) { wire.Build(NewEvent, NewPeople, NewMessage) return Event{}, nil } func main() { e, err := InitializeEvent() if err != nil { fmt.Printf("failed to create event: %s\n", err) os.Exit(2) } e.start() } ``` 更改之後的程式碼初始化NewEvent 可能就會因為People.grumpy 的值而失敗,通過wire生成之後的程式碼 ```go // Injectors from main.go: func InitializeEvent() (Event, error) { message := NewMessage() people := NewPeople(message) event, err := NewEvent(people) if err != nil { return Event{}, err } return event, nil } ``` ### 栗子3 我們再將上面的程式碼進行更改: ```go package main import ( "errors" "fmt" "os" "time" "github.com/google/wire" ) func NewMessage(msg string) Message { return Message(msg) } // 要說的內容的抽象 type Message string func NewPeople(m Message) People { var grumpy bool if time.Now().Unix()%2 == 0 { grumpy = true } return People{name: "小明", message: m, grumpy: grumpy} } // 小明這個人的抽象 type People struct { name string message Message grumpy bool // 脾氣是否暴躁 } // 小明這個人會說話 func (p People) SayHello() string { if p.grumpy { // 脾氣暴躁,心情不好 msg := "Go away !" return msg } msg := fmt.Sprintf("%s 對世界說:%s\n", p.name, p.message) return msg } func NewEvent(p People) (Event, error) { if p.grumpy { return Event{}, errors.New("could not create event: event greeter is grumpy") } return Event{people: p}, nil } // 小明去說話這個行為抽象為一個事件 type Event struct { people People } func (e Event) start() { msg := e.people.SayHello() fmt.Println(msg) } func InitializeEvent(msg string) (Event, error) { wire.Build(NewEvent, NewPeople, NewMessage) return Event{}, nil } func main() { msg := "Hello Golang"https://github.com/LyricTian/gin-admin e, err := InitializeEvent(msg) if err != nil { fmt.Printf("failed to create event: %s\n", err) os.Exit(2) } e.start() } ``` 上面的更改主要是NewPeople 函式增加了msg引數,同時InitializeEvent增加了msg引數,這個時候我們通過wire生成程式碼則可以看到如下: ```go // Injectors from main.go: func InitializeEvent(msg string) (Event, error) { message := NewMessage(msg) people := NewPeople(message) event, err := NewEvent(people) if err != nil { return Event{}, err } return event, nil } ``` wire 會檢查注入器的引數,並檢查到NewMessage 需要msg的引數,所以它將msg傳遞給了NewMessage ### 栗子4 如果我們傳給wire.Build 的依賴關係存在問題,wire會怎麼處理呢? 我們調整InitializeEvent 的程式碼: ```go func InitializeEvent(msg string) (Event, error) { wire.Build(NewEvent, NewMessage) return Event{}, nil } ``` 然後執行wire 進行程式碼的生成: ```bash ➜ useWireBaseExample4 wire . wire: /home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample4/main.go:63:1: inject InitializeEvent: no provider found for awesomeProject/202006/useWireBaseExample4.People needed by awesomeProject/202006/useWireBaseExample4.Event in provider "NewEvent" (/home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample4/main.go:46:6) wire: awesomeProject/202006/useWireBaseExample4: generate failed wire: at least one generate failure ➜ useWireBaseExample4 ``` 錯誤提示中非常清楚的告訴我它找不到no provider found ,如果我們傳給wire.Build 沒有用的依賴,它依然會給我們提示告訴我們 `unused provider "main.NewEventNumber"` ```bash ➜ useWireBaseExample4 wire . wire: /home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample4/main.go:67:1: inject InitializeEvent: unused provider "main.NewEventNumber" wire: awesomeProject/202006/useWireBaseExample4: generate failed wire: at least one generate failure ``` ## wire的高階用法 ### Binding Interfaces 依賴注入通常用於繫結介面的具體實現。通過下面的例子理解: ```go // Run 執行服務 func Run(ctx context.Context, opts ...Option) error { var state int32 = 1 sc := make(chan os.Signal, 1) signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) cleanFunc, err := Init(ctx, opts...) if err != nil { return err } EXIT: for { sig := <-sc logger.Printf(ctx, "接收到訊號[%s]", sig.String()) switch sig { case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT: atomic.CompareAndSwapInt32(&state, 1, 0) break EXIT case syscall.SIGHUP: default: break EXIT } } cleanFunc() logger.Printf(ctx, "服務退出") time.Sleep(time.Second) os.Exit(int(atomic.LoadInt32(&state))) return nil }package main import ( "fmt" "github.com/google/wire" ) type Fooer interface { Foo() string } type MyFooer string func (b *MyFooer) Foo() string { return string(*b) } func provideMyFooer() *MyFooer { b := new(MyFooer) *b = "Hello, World!" return b } type Bar string func provideBar(f Fooer) string { // f will be a *MyFooer. return f.Foo() } func InitializeEvent() string { wire.Build(provideMyFooer, provideBar, wire.Bind(new(Fooer), new(*MyFooer))) return "" } func main() { ret := InitializeEvent() fmt.Println(ret) } ``` 我們可以看到`Fooer` 是一個interface, `MyFooer` 實現了`Fooer` 這個介面,同時`provideBar` 的引數是`Fooer` 介面型別。可以看到// Run 執行服務 ```go func Run(ctx context.Context, opts ...Option) error { var state int32 = 1 sc := make(chan os.Signal, 1) signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) cleanFunc, err := Init(ctx, opts...) if err != nil { return err } EXIT: for { sig := <-sc logger.Printf(ctx, "接收到訊號[%s]", sig.String()) switch sig { case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT: atomic.CompareAndSwapInt32(&state, 1, 0) break EXIT case syscall.SIGHUP: default: break EXIT } } logger.Printf(ctx, "服務退出") time.Sleep(time.Second) os.Exit(int(atomic.LoadInt32(&state))) return nil } ``` 程式碼中我們用了`wire.Bind`方法,為什麼這麼用呢?如果我們`wire.Build`的那段程式碼寫成如下: `wire.Build(provideMyFooer, provideBar)`,再次用wire生成程式碼則會提示如下錯誤:https://github.com/LyricTian/gin-admin ```shell ➜ useWireBaseExample5 wire . wire: /home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample5/main.go:36:1: inject InitializeEvent: no provider found for awesomeProject/202006/useWireBaseExample5.Fooer needed by string in provider "provideBar" (/home/fan/codes/go_project/awesomeProject/202006/useWireBaseExample5/main.go:27:6) wire: awesomeProject/202006/useWireBaseExample5: generate failed wire: at least one generate failure ``` 這是因為我們傳遞給`provideBar` 需要的是 `Fooer` 介面型別,我們傳給`wire.Build` 的是`provideMyFooer, provideBar` 這個時候預設從依賴關係裡,`provideBar` 沒有找能夠提供`Fooer`的provider, 雖然我們我們都知道`MyFooer` 實現了`Fooer` 這個介面。所以我們需要在`wire.Build` 裡告訴它,我們傳遞`provideMyFooer` 就是`provideBar`的provider。`wire.Bind` 就是來做這件事情的。 `wire.Bind` 的第一個引數是介面型別的值的指標,第二個引數是實現第一個引數介面的型別的值的指標。 這樣當我們在用wire生成程式碼的時候就正常了。 ### Struct Providers wire還可以用於結構體的構造。先直接看使用的例子: ```go package main import ( "fmt" "github.com/google/wire" ) type Foo int type Bar int func ProvideFoo() Foo { return Foo(1) } func ProvideBar() Bar { return Bar(2)// Run 執行服務 func Run(ctx context.Context, opts ...Option) error { var state int32 = 1 sc := make(chan os.Signal, 1) signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) cleanFunc, err := Init(ctx, opts...) if err != nil { return err } EXIT: for { sig := <-sc logger.Printf(ctx, "接收到訊號[%s]", sig.String()) switch sig { case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT: atomic.CompareAndSwapInt32(&state, 1, 0) break EXIT case syscall.SIGHUP: default: break EXIT } } cleanFunc() logger.Printf(ctx, "服務退出") time.Sleep(time.Second) os.Exit(int(atomic.LoadInt32(&state))) return nil } } type FooBar struct { MyFoo Foo MyBar Bar } var Set = wire.NewSet( ProvideFoo, ProvideBar, wire.Struct(new(FooBar), "MyFoo", "MyBar"), ) func injectFooBar() FooBar { wire.Build(Set) return FooBar{} } func main() { fooBar := injectFooBar() fmt.Println(fooBar) } ``` 上面的例子其實很簡單,我們構造`FooBar` 結構題我們需要`MyFoo` 和 `MyBar` ,而`ProvideFoo` 和 `ProvideBar` 就是用於生成`MyFoo` 和 `MyBar`,`wire.Struct ` 也可以幫我們做這件事情。我們通過wire生成的程式碼如下: ```go // Injectors from main.go: func injectFooBar() FooBar { foo := ProvideFoo() bar := ProvideBar() fooBar := FooBar{ MyFoo: foo, MyBar: bar, } return fooBar } ``` `wire.Struct` 的第一個引數是所需結構型別的指標,後面的引數是要注入的欄位的名稱。可以使用一個特殊的字串“ * ”作為告訴注入器注入所有欄位的快捷方式。 所以我們上面的程式碼也可以寫成:`wire.Struct(new(FooBar), "×")` ,而當我們使用`*` 這種方式的時候可能會把一些不需要注入的欄位注入了,如鎖,那麼類似這種情況,如果我們注入,卡一通過`wire:"-"` 的方式告訴wire 該欄位不進行注入。 ```go type Foo struct { mu sync.Mutex `wire:"-"` Bar Bar } ``` ### Binding Values 這個功能主要就是給資料型別繫結一個預設值,程式碼例子如下: ```go https://github.com/LyricTian/gin-adminpackage main import ( "fmt" "github.com/google/wire" ) type Foo struct { X int } func injectFoo() Foo { wire.Build(wire.Value(Foo{X: 11})) return Foo{} } func main() { foo := injectFoo() fmt.Println(foo) } ``` 我通過wire生成的程式碼如下: ```go // Code generated by Wire. DO NOT EDIT. //go:generate wire //+build !wireinject package main import ( "fmt" ) // Injectors from main.go: func injectFoo() Foo { foo := _wireFooValue return foo } var ( _wireFooValue = Foo{X: 11} ) // main.go: type Foo struct { X int } func main() { foo := injectFoo() fmt.Println(foo) } ``` ### Use Fields of a Struct as Providers 有時,我們需要獲取結構體的某些欄位,按照我們已經使用的wire的用法,你可能會這樣寫程式碼: ```go package main import ( "fmt" "github.com/google/wire" ) type Foo struct { S string N int F float64 } func getS(foo Foo) string { return foo.S } func provideFoo() Foo { return Foo{S: "Hello, World!", N: 1, F: 3.14} } func injectedMessage() string { wire.Build( provideFoo, getS, ) return "" } func main() { ret := injectedMessage() fmt.Println(ret) } ``` 這種用法當然也可以實現,但是wire其實提供了更好的辦法來實現`wire.FieldsOf`, 我們將上面的程式碼進行更改如下,通過wire生成的程式碼其實和上面的是一樣的: ```go package main import ( "fmt" "github.com/google/wire" ) type Foo struct { S string N int F float64 } func provideFoo() Foo { return Foo{S: "Hello, World!", N: 1, F: 3.14} } func injectedMessage() string { wire.Build( provideFoo, wire.FieldsOf(new(Foo), "S"), ) return "" } func main() { ret := injectedMessage() fmt.Println(ret) } ``` ### Cleanup functions 如果我們的Provider建立了一個需要做clean 的值,例如關閉檔案,關閉資料連線..., 這裡也是可以返回一個閉包來清理資源,注入器將使用它向呼叫者返回一個聚合的清理函式,或者如果稍後在注入器實現中呼叫的提供程式返回一個錯誤,則使用它來清理資源。 關於這個功能的使用,通過https://github.com/LyricTian/gin-admin 的程式碼中的使用,可以更加清楚。 作者在gin-admin/internal/app/app.go 中進行了初始化依賴注入器 ```go // 初始化依賴注入器 injector, injectorCleanFunc, err := injector.BuildInjector() if err != nil { return nil, err } ``` 我們在看看下wire生成的wire_gen.go程式碼: ```go // Injectors from wire.go: func BuildInjector() (*Injector, func(), error) { auther, cleanup, err := InitAuth() if err != nil { return nil, nil, err } db, cleanup2, err := InitGormDB() if err != nil { cleanup() return nil, nil, err } role := &model.Role{ DB: db, } roleMenu := &model.RoleMenu{ DB: db, } menuActionResource := &model.MenuActionResource{ DB: db, } user := &model.User{ DB: db, } userRole := &model.UserRole{ DB: db, } casbinAdapter := &adapter.CasbinAdapter{ RoleModel: role, RoleMenuModel: roleMenu, MenuResourceModel: menuActionResource, UserModel: user, UserRoleModel: userRole, } syncedEnforcer, cleanup3, err := InitCasbin(casbinAdapter) if err != nil { cleanup2() cleanup() return nil, nil, err } demo := &model.Demo{ DB: db, } bllDemo := &bll.Demo{ DemoModel: demo, } apiDemo := &api.Demo{ DemoBll: bllDemo, } menu := &model.Menu{ DB: db, } menuAction := &model.MenuAction{ DB: db, } login := &bll.Login{ Auth: auther, UserModel: user, UserRoleModel: userRole, RoleModel: role, RoleMenuModel: roleMenu, MenuModel: menu, MenuActionModel: menuAction, } apiLogin := &api.Login{ LoginBll: login, } trans := &model.Trans{ DB: db, } bllMenu := &bll.Menu{ TransModel: trans, MenuModel: menu, MenuActionModel: menuAction, MenuActionResourceModel: menuActionResource, } apiMenu := &api.Menu{ MenuBll: bllMenu, } bllRole := &bll.Role{ Enforcer: syncedEnforcer, TransModel: trans, RoleModel: role, RoleMenuModel: roleMenu, UserModel: user, } apiRole := &api.Role{ RoleBll: bllRole, } bllUser := &bll.User{ Enforcer: syncedEnforcer, TransModel: trans, UserModel: user, UserRoleModel: userRole, RoleModel: role, } apiUser := &api.User{ UserBll: bllUser, } routerRouter := &router.Router{ Auth: auther, CasbinEnforcer: syncedEnforcer, DemoAPI: apiDemo, LoginAPI: apiLogin, MenuAPI: apiMenu, RoleAPI: apiRole, UserAPI: apiUser, } engine := InitGinEngine(routerRouter) injector := &Injector{ Engine: engine, Auth: auther, CasbinEnforcer: syncedEnforcer, MenuBll: bllMenu, } return injector, func() { cleanup3() cleanup2() cleanup() }, nil } ``` 而當程式退出的時候這上面程式碼返回的那些清理操作都會被執行: ```go // Run 執行服務 func Run(ctx context.Context, opts ...Option) error { var state int32 = 1 sc := make(chan os.Signal, 1) signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) cleanFunc, err := Init(ctx, opts...) if err != nil { return err } EXIT: for { sig := <-sc logger.Printf(ctx, "接收到訊號[%s]", sig.String()) switch sig { case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT: atomic.CompareAndSwapInt32(&state, 1, 0) break EXIT case syscall.SIGHUP: default: break EXIT } } // 在這裡執行了清理工作 cleanFunc() logger.Printf(ctx, "服務退出") time.Sleep(time.Second) os.Exit(int(atomic.LoadInt32(&state))) return nil } ``` ## 延伸閱讀 - https://github.com/goo