wire 源码分析
Posted golang算法架构leetcode技术php
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了wire 源码分析相关的知识,希望对你有一定的参考价值。
Wire 是一个轻巧的 Golang 依赖注入工具。它由 Go Cloud 团队开发,通过自动生成代码的方式在编译期完成依赖注入。Uber 的 dig 、来自 Facebook 的 inject 。他们都通过反射机制实现了运行时依赖注入。
Wire 生成的代码与手写无异。这种方式带来一系列好处:
方便 debug,若有依赖缺失编译时会报错
因为不需要 Service Locators, 所以对命名没有特殊要求
避免依赖膨胀。生成的代码只包含被依赖的代码,而运行时依赖注入则无法作到这一点
依赖关系静态存于源码之中, 便于工具分析与可视化
运行go get github.com/google/wire/cmd/wire
之后, wire
命令行工具 将被安装到 $GOPATH/bin
。
wire 中的两个核心概念:Provider 和 Injector:
Provider: 生成组件的普通方法。这些方法接收所需依赖作为参数,创建组件并将其返回。
组件可以是对象或函数 —— 事实上它可以是任何类型,但单一类型在整个依赖图中只能有单一 provider。因此返回 int
类型的 provider 不是个好主意。对于这种情况, 可以通过定义类型别名来解决。例如先定义type Category int
,然后让 provider 返回 Category
类型
// DefaultConnectionOpt provide default connection option
func DefaultConnectionOpt()*ConnectionOpt{...}// NewDb provide an Db object
func NewDb(opt *ConnectionOpt)(*Db, error){...}// NewUserLoadFunc provide a function which can load user
func NewUserLoadFunc(db *Db)(func(int) *User, error){...}
一组业务相关的 provider 时常被放在一起组织成 ProviderSet,以方便维护与切换。
var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)
Injector: 由wire
自动生成的函数。函数内部会按根据依赖顺序调用相关 privoder 。
为了生成此函数, 我们在 wire.go
(文件名非强制,但一般约定如此)文件中定义 injector 函数签名。然后在函数体中调用wire.Build
,并以所需 provider 作为参数(无须考虑顺序)。
由于wire.go
中的函数并没有真正返回值,为避免编译器报错, 简单地用panic
函数包装起来即可。不用担心执行时报错, 因为它不会实际运行,只是用来生成真正的代码的依据。
wire.go
// +build wireinject
//上面这个条件编译tag可以避免生成代码重名函数报错
package main
import "github.com/google/wire"
func UserLoader()(func(int)*User, error){
panic(wire.Build(NewUserLoadFunc, DbSet))
}
var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)
运行 wire
命令将生成 wire_gen.go 文件,其中保存了 injector 函数的真正实现。wire.go 中若有非 injector 的代码将被原样复制到 wire_gen.go 中(虽然技术上允许,但不推荐这样作)。
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package main
import (
"github.com/google/wire"
)
// Injectors from wire.go:
func UserLoader() (func(int) *User, error) {
connectionOpt := DefaultConnectionOpt()
db, err := NewDb(connectionOpt)
if err != nil {
return nil, err
}
v, err := NewUserLoadFunc(db)
if err != nil {
return nil, err
}
return v, nil
}
// wire.go:
var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)
上述代码有两点值得关注:
wire.go 第一行
// ***+build\*** wireinject
,这个 build tag 确保在常规编译时忽略 wire.go 文件(因为常规编译时不会指定wireinject
标签)。与之相对的是 wire_gen.go 中的//***+build\*** !wireinject
。两组对立的 build tag 保证在任意情况下, wire.go 与 wire_gen.go 只有一个文件生效, 避免了“UserLoader 方法被重复定义”的编译错误自动生成的 UserLoader 代码包含了 error 处理。与我们手写代码几乎相同。对于这样一个简单的初始化过程, 手写也不算麻烦。但当组件数达到几十、上百甚至更多时, 自动生成的优势就体现出来了。
要触发“生成”动作有两种方式:go generate
或 wire
。前者仅在 wire_gen.go 已存在的情况下有效(因为 wire_gen.go 的第三行 //***go:generate\*** wire
),而后者在任何时候都有可以调用。并且后者有更多参数可以对生成动作进行微调, 所以建议始终使用 wire
命令。
然后我们就可以使用真正的 injector 了, 例如:
package main
import "log"
func main() {
fn, err := UserLoader()
if err != nil {
log.Fatal(err)
}
user := fn(123)
...
}
如果不小心忘记了某个 provider, wire
会报出具体的错误, 帮忙开发者迅速定位问题。例如我们修改 wire.go
,去掉其中的NewDb
// +build wireinject
package main
import "github.com/google/wire"
func UserLoader()(func(int)*User, error){
panic(wire.Build(NewUserLoadFunc, DbSet))
}
var DbSet = wire.NewSet(DefaultConnectionOpt) //forgot add Db provider
将会报出明确的错误:“no provider found for *example.Db
”
wire: /usr/example/wire.go:7:1: inject UserLoader: no provider found for *example.Db
needed by func(int) *example.User in provider "NewUserLoadFunc" (/usr/example/provider.go:24:6)
wire: example: generate failed
wire: at least one generate failure
同样道理, 如果在 wire.go 中写入了未使用的 provider , 也会有明确的错误提示。
高级功能
谈过基本用法以后, 我们再看看高级功能
*接口注入*
有时需要自动注入一个接口, 这时有两个选择:
较直接的作法是在 provider 中生成具体类, 然后返回接口类型。但这不符合Golang 代码规范。一般不采用
让 provider 返回具体类,但在 injector 声明环节作文章,将类绑定成接口,例如:
// FooInf, an interface
// FooClass, an class which implements FooInf
// fooClassProvider, a provider function that provider *FooClassvar set = wire.NewSet(
fooClassProvider,
wire.Bind(new(FooInf), new(*FooClass) // bind class to interface
)
*属性自动注入*
有时我们不需什么特定的初始化工作, 只是简单地创建一个对象实例, 为其指定属性赋值,然后返回。当属性多的时候,这种工作会很无聊。
// provider.gotype App struct {
Foo *Foo
Bar *Bar
}func DefaultApp(foo *Foo, bar *Bar)*App{
return &App{Foo: foo, Bar: bar}
}
// wire.go
...
wire.Build(provideFoo, provideBar, DefaultApp)
...
wire.Struct
可以简化此类工作, 指定属性名来注入特定属性:
wire.Build(provideFoo, provideBar, wire.Struct(new(App),"Foo","Bar")
如果要注入全部属性,则有更简化的写法:
wire.Build(provideFoo, provideBar, wire.Struct(new(App), "*")
如果 struct 中有个别属性不想被注入,那么可以修改 struct 定义:
type App struct {
Foo *Foo
Bar *Bar
NoInject int `wire:"-"`
}
这时 NoInject
属性会被忽略。与常规 provider 相比, wire.Struct
提供一项额外的灵活性:它能适应指针与非指针类型,根据需要自动调整生成的代码。
大家可以看到wire.Struct
的确提供了一些便利。但它要求注入属性可公开访问, 这导致对象暴露本可隐藏的细节。
好在这个问题可以通过上面提到的“接口注入”来解决。用 wire.Struct
创建对象,然后将其类绑定到接口上。至于在实践中如何权衡便利性和封装程度,则要具体情况具体分析了。
*值绑定*
虽不常见,但有时需要为基本类型的属性绑定具体值, 这时可以使用 wire.Value
:
// provider.go
type Foo struct {
X int
}// wire.go
...
wire.Build(wire.Value(Foo{X: 42}))
...
为接口类型绑定具体值,可以使用 wire.InterfaceValue
:
wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
*把对象属性用作 Provider*
有时我们只是需要用某个对象的属性作为 Provider,例如
// provider
func provideBar(foo Foo)*Bar{
return foo.Bar
}
// injector
...
wire.Build(provideFoo, provideBar)
...
这时可以用 wire.FieldsOf
加以简化,省掉啰嗦的 provider:
wire.Build(provideFoo, wire.FieldsOf(new(Foo), "Bar"))
与 wire.Struct
类似, wire.FieldsOf
也会自动适应指针/非指针的注入请求
*清理函数*
前面提到若 provider 和 injector 函数有返回错误, 那么 wire 会自动处理。除此以外,wire 还有另一项自动处理能力:清理函数。
所谓清理函数是指型如 func()
的闭包, 它随 provider 生成的组件一起返回, 确保组件所需资源可以得到清理。
清理函数典型的应用场景是文件资源和网络连接资源,例如:
type App struct {
File *os.File
Conn net.Conn
}
func provideFile() (*os.File, func(), error) {
f, err := os.Open("foo.txt")
if err != nil {
return nil, nil, err
}
cleanup := func() {
if err := f.Close(); err != nil {
log.Println(err)
}
}
return f, cleanup, nil
}
func provideNetConn() (net.Conn, func(), error) {
conn, err := net.Dial("tcp", "foo.com:80")
if err != nil {
return nil, nil, err
}
cleanup := func() {
if err := conn.Close(); err != nil {
log.Println(err)
}
}
return conn, cleanup, nil
}
上述代码定义了两个 provider 分别提供了文件资源和网络连接资源
wire.go
// +build wireinject
package main
import "github.com/google/wire"
func NewApp() (*App, func(), error) {
panic(wire.Build(
provideFile,
provideNetConn,
wire.Struct(new(App), "*"),
))
}
注意由于 provider 返回了清理函数, 因此 injector 函数签名也必须返回,否则将会报错
wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package main
// Injectors from wire.go:
func NewApp() (*App, func(), error) {
file, cleanup, err := provideFile()
if err != nil {
return nil, nil, err
}
conn, cleanup2, err := provideNetConn()
if err != nil {
cleanup()
return nil, nil, err
}
app := &App{
File: file,
Conn: conn,
}
return app, func() {
cleanup2()
cleanup()
}, nil
}
生成代码中有两点值得注意:
当
provideNetConn
出错时会调用cleanup()
, 这确保了即使后续处理出错也不会影响前面已分配资源的清理。最后返回的闭包自动组合了
cleanup2()
和cleanup()
。意味着无论分配了多少资源, 只要调用过程不出错,他们的清理工作就会被集中到统一的清理函数中。最终的清理工作由 injector 的调用者负责
可以想像当几十个清理函数的组合在一起时, 手工处理上述两个场景是非常繁琐且容易出错的。wire 的优势再次得以体现。
然后就可以使用了:
func main() {
app, cleanup, err := NewApp()
if err != nil {
log.Fatal(err)
}
defer cleanup()
...
}
注意 main 函数中的 defer cleanup()
,它确保了所有资源最终得到回收
以上是关于wire 源码分析的主要内容,如果未能解决你的问题,请参考以下文章
Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段
Android 事件分发事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )(代码片段
mysql jdbc源码分析片段 和 Tomcat's JDBC Pool
Android 逆向ART 脱壳 ( DexClassLoader 脱壳 | DexClassLoader 构造函数 | 参考 Dalvik 的 DexClassLoader 类加载流程 )(代码片段
Android 逆向ART 脱壳 ( DexClassLoader 脱壳 | DexClassLoader 构造函数 | 参考 Dalvik 的 DexClassLoader 类加载流程 )(代码片段