Golang 使用 Wire 进行依赖注入
前言
在参与 Star Horizon 的一个项目时,我得到团队内同学的启发,发现了 Wire 这个神奇的东西。
Wire 是 Google 研发并开源的一个 Golang 依赖注入解决方案,它通过解释原有文件生成新文件并用 Go Build Injector 实现编译环节的代码区分。
根据 Go Blog 所属,Wire 最先用于 Google 开源的 Go Cloud 项目中。
应用
场景
我们通常会在构建 SDK 时在 Client 中嵌套 Services,在这种场景下 Client 依赖于 Services。
但常见的问题是:随着 Services 的增多,代码变得越来越冗杂,甚至增加一个 Service 就要进行数十行的代码变更。而一旦有新人参与开发,很容易漏掉一些必须的代码变更从而引起 Bug 的产生。
Google 自然也是面临这种问题,毕竟其体量巨大。为此,他们创造了 Wire 用来解决依赖注入问题。
安装
首先,我们需要在项目中引入 github.com/google/wire
这一 Package 进行标识,同时在全局安装:
# 项目中添加依赖
go get -u github.com/google/wire
# 全局安装 Wire 的命令行程序
go install github.com/google/wire/cmd/wire@latest
随后,我们可以通过 wire
调用命令行程序:
Wire 进行依赖注入的过程
当然,调用之前需要我们先进行完下面的步骤。
差异
传统依赖注入方法
通常情况下,在 pkg 中配置 Client
我们会进行如下操作
// service/foo.go
package service
import "fmt"
type Foo interface {
Foo()
}
type foo struct {
}
func (f *foo) Foo() {
fmt.Println("foo")
}
func NewFoo() Foo {
return &foo{}
}
// service/bar.go
package service
import "fmt"
type Bar interface {
Bar()
}
type bar struct {
}
func (b *bar) Bar() {
fmt.Println("bar")
}
func NewBar() Bar {
return &bar{}
}
// service/client.go
package service
type Client struct {
Foo Foo
Bar Bar
}
func NewClient(foo Foo, bar Bar) *Client {
return &Client{
Foo: foo,
Bar: bar,
}
}
// main.go
package main
import "wire_tutorial/service"
func main() {
foo := service.NewFoo()
bar := service.NewBar()
client := service.NewClient(foo, bar)
client.Foo.Foo()
client.Bar.Bar()
}
此时编译执行当前目录我们会得到如下结果:
在当前场景,
Client
依赖于Foo
和Bar
。这是一个非常微小的Client
,所以可能并不是很能体现依赖注入的问题。
使用 Wire
在原有代码不变的基础下,我们新增了 service/wire.go
并修改了 main.go
:
// service/wire.go
package service
import "github.com/google/wire"
func BuildClient() *Client {
wire.Build(NewClient, NewFoo, NewBar)
return nil
}
// main.go
package main
import "wire_tutorial/service"
func main() {
client := service.BuildClient()
client.Foo.Foo()
client.Bar.Bar()
}
当前情况下,如果你编译运行会发现,因为 Client
的属性 Foo
Bar
都为 nil
,因此会直接报出 panic: runtime error: invalid memory address or nil pointer dereference
的错误。
此时,我们在终端执行 wire ./...
:
wire ./...
# 或
go run github.com/google/wire/cmd/wire@latest ./...
会发现 Wire 创造了一个新文件:service/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
// 需要注意,上边的两行叫做 Injector,其意义是在执行 go build 的时候如果携带 wireinject 的 arg 就不编译当前文件
package service
// Injectors from wire.go:
func BuildClient() *Client {
serviceFoo := NewFoo()
serviceBar := NewBar()
client := NewClient(serviceFoo, serviceBar)
return client
}
此时如果你再进行编译会发现仍然无法正常运行,因为 service/wire.go
和 service/wire_gen.go
的 BuildClient() *Client
函数起了冲突,而此时我们可以根据 Google 的实例,通过 Injector 来避免项目打包时同时编译两个文件:
// service/wire.go
//go:build wireinject
// +build wireinject
package service
import "github.com/google/wire"
func BuildClient() *Client {
wire.Build(NewClient, NewFoo, NewBar)
return nil
}
此时,只有携带 wireinject
的参数进行编译时,当前文件才会被编译在软件内。
再次编译,发现程序可以正常输出,此时 Wire 的配置就完成了。
编译目录
日后只有在修改 service/wire.go
时才需要重新生成 service/wire_gen.go
,因此根据 Google
的文档所述,你可以将 service/wire_gen.go
带入版本控制系统而非每次重新生成。
更多
参数传递
Wire 对于参数传递具有限制。
例如,对于以下情况的代码,他无法生成 wire_gen.go
文件:
// service/client.go
package service
import "fmt"
type Client struct {
Foo Foo
Bar Bar
}
var Debug bool
func NewClient(foo Foo, bar Bar) *Client {
if Debug {
fmt.Println("debug mode")
}
return &Client{
Foo: foo,
Bar: bar,
}
}
// service/wire.go
//go:build wireinject
// +build wireinject
package service
import "github.com/google/wire"
func BuildClient() *Client {
wire.Build(Debug, NewClient, NewFoo, NewBar)
return &Client{}
}
// service/main.go
package main
import (
"flag"
"wire_tutorial/service"
)
var (
debug bool
)
func init() {
flag.BoolVar(&debug, "debug", false, "debug mode")
flag.Parse()
}
func main() {
service.Debug = debug
client := service.BuildClient()
client.Foo.Foo()
client.Bar.Bar()
}
此时,执行 wire ./...
会出现以下错误:
这是因为 Wire 将 NewClient 这类函数当做 Provider,而 Debug 这个变量并非一个合法的 Provider。
我们只需做如下操作:
// service/client.go
package service
import "fmt"
type Client struct {
Foo Foo
Bar Bar
}
// 注意此处将全局变量 Debug 移动到了函数参数内
func NewClient(debug bool, foo Foo, bar Bar) *Client {
if debug {
fmt.Println("debug mode")
}
return &Client{
Foo: foo,
Bar: bar,
}
}
// service/wire.go
//go:build wireinject
// +build wireinject
package service
import "github.com/google/wire"
// 此处新增了和上方类型名称一致的形参 debug
func BuildClient(debug bool) *Client {
wire.Build(NewClient, NewFoo, NewBar)
return &Client{}
}
// main.go
package main
import (
"flag"
"wire_tutorial/service"
)
var (
debug bool
)
func init() {
flag.BoolVar(&debug, "debug", false, "debug mode")
flag.Parse()
}
func main() {
client := service.BuildClient(debug)
client.Foo.Foo()
client.Bar.Bar()
}
此时重新执行 wire ./...
,会发现 service/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 service
// Injectors from wire.go:
func BuildClient(debug bool) *Client {
serviceFoo := NewFoo()
serviceBar := NewBar()
client := NewClient(debug, serviceFoo, serviceBar)
return client
}
此时,功能可正常使用。
鸣谢
Google 提供的文档用于理解 Wire 的基本原理。
- https://github.com/google/wire/blob/main/\_tutorial/README.md
- https://github.com/google/wire/blob/main/docs/guide.md
- https://go.dev/blog/wire
- https://github.com/google/wire/blob/main/docs/best-practices.md
@topjohncian 使我了解 Wire 和其基本使用方法。