Google的 DI 框架 Wire
本文最后更新于 760 天前,其中的信息可能已经有所发展或是发生改变。

前言

以下内容来自 Wire 官方文档,花了一天把英文的 readme 啃了遍,发现存在几个问题:

  1. 记住的不多

  2. 后面遇到问题需要再来看readme,但是个人英语阅读效率太低,又要花很多时间

  3. 将来复习时,去看英文文档,没有中文的直观。

所以在这里记录以下。

介绍

Wire是一个代码生成工具,使用依赖注入自动连接组件。组件之间的依赖关系在Wire中表示为函数参数,鼓励显式初始化,而不是全局变量。因为Wire没有运行时状态或反射,所以编写用于Wire的代码即使对于手工编写的初始化也很有用。

如果使用全局变量,可以在运行期间动态获取到依赖,但会导致依赖关系不固定。

使用示例

未使用依赖注入

模拟一个活动,让迎宾员用一条特定的信息向客人致意。

在本设计中,有三种结构类型:

// 为迎宾者发送的消息
type Message string

// 传达该消息的迎宾者
type Greeter struct {
    Message Message 
}

// 迎宾者说出问候语
func (g Greeter) Greet() Message {
    return g.Message
}

// 以迎宾者问候客人开始的活动
type Event struct {
    Greeter Greeter
}

// 活动开始
func (e Event) Start() {
    msg := e.Greeter.Greet()
    fmt.Println(msg)
}

相应的初始化函数

func NewMessage() Message {
    return Message("Hi there!")
}

func NewGreeter(m Message) Greeter {
    return Greeter{Message: m}
}

func NewEvent(g Greeter) Event {
    return Event{Greeter: g}
}

主函数

func main() {
    message := NewMessage()
    greeter := NewGreeter(message)
    event := NewEvent(greeter)

    event.Start()
}
  • 这是简单示例,如果是实际项目中,依赖关系复杂,这个main函数的内容很难写。

使用 Wire

使用依赖注入设计原则,传递每个组件所需的任何信息。有助于编写易于测试的代码,并且易于替换实现类,提高扩展性。

主函数

func main() {
    e := InitializeEvent()

    e.Start()
}

需要编写的 wire.go

//+build wireinject

func InitializeEvent() Event {
    wire.Build(NewEvent, NewGreeter, NewMessage)
    return Event{}
}
  • //+build wireinject:此文件的目的是说明需要使用哪些提供provider函数,因此需要在文件顶部标识,使用构建约束将其从最终构建后的文件中排除

使用 wire 命令工具

安装

go get github.com/google/wire/cmd/wire

使用
  • wire:生成依赖注入代码wire_gen.go
  • go generate:当存在wire_gen.go时,也就是非初次,还可使用这个命令

生成的依赖注入代码 wire_gen.go

func InitializeEvent() Event {
    message := NewMessage()
    greeter := NewGreeter(message)
    event := NewEvent(greeter)
    return event
}

支持返回异常

func NewEvent(g Greeter) (Event, error) {
    if g.Grumpy {
        return Event{}, errors.New("could not create event: event greeter is grumpy")
    }
    return Event{Greeter: g}, nil
}

主函数

func main() {
    e, err := InitializeEvent()
    if err != nil {
        fmt.Printf("failed to create event: %s\n", err)
        os.Exit(2)
    }
    e.Start()
}

依赖注入声明函数

func InitializeEvent() (Event, error) {
    wire.Build(NewEvent, NewGreeter, NewMessage)
    return Event{}, nil
}

生成的依赖注入代码

func InitializeEvent() (Event, error) {
    message := NewMessage()
    greeter := NewGreeter(message)
    event, err := NewEvent(greeter)
    if err != nil {
        return Event{}, err
    }
    return event, nil
}

给依赖提供参数

需要参数的依赖

func NewMessage(phrase string) Message {
    return Message(phrase)
}

依赖注入声明函数

func InitializeEvent(phrase string) (Event, error) {
    wire.Build(NewEvent, NewGreeter, NewMessage)
    return Event{}, nil
}

生成的依赖注入代码

func InitializeEvent(phrase string) (Event, error) {
    message := NewMessage(phrase)
    greeter := NewGreeter(message)
    event, err := NewEvent(greeter)
    if err != nil {
        return Event{}, err
    }
    return event, nil
}

进阶使用

Provider 分组

如果将所有的 Provider 都用到一个依赖注入声明函数里,会很臃肿,不便于管理。可以根据包来对 Provider 进行分组,每个包下都有一个 ProviderSet,使包里的依赖注入更清晰。

子包下的 wire.go

var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)

主包下的 wire.go

func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    wire.Build(foobarbaz.SuperSet)
    return foobarbaz.Baz{}, nil
}

接口绑定实现类

接口

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()
}

依赖注入声明函数

var Set = wire.NewSet(
    provideMyFooer,
    wire.Bind(new(Fooer), new(*MyFooer)),
    provideBar)

Struct Providers

不提供Struct 的 Provider,直接注入字段

type Foo int
type Bar int

func ProvideFoo() Foo {/* ... */}

func ProvideBar() Bar {/* ... */}

type FooBar struct {
    MyFoo Foo
    MyBar Bar
}

var Set = wire.NewSet(
    ProvideFoo,
    ProvideBar,
    wire.Struct(new(FooBar), "MyFoo", "MyBar"))

注入所有字段

wire.Struct(new(FooBar), "*")

排除个别字段
type Foo struct {
    mu sync.Mutex `wire:"-"`
    Bar Bar
}

提供初始值

这是没有写 Provider 的情况,也可以通过 Provider 来指定初始值

type Foo struct {
    X int
}

func injectFoo() Foo {
    wire.Build(wire.Value(Foo{X: 42}))
    return Foo{}
}

生成的依赖注入代码

func injectFoo() Foo {
    foo := _wireFooValue
    return foo
}

var (
    _wireFooValue = Foo{X: 42}
)
  • 这种方式指定初始值,不能调用任何函数,以及从管道里获取数据

给接口提供初始值

func injectReader() io.Reader {
    wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
    return nil
}

字段作为依赖

type Foo struct {
    S string
    N int
    F float64
}

func getS(foo Foo) string {
    // Bad! Use wire.FieldsOf instead.
    return foo.S
}

func provideFoo() Foo {
    return Foo{ S: "Hello, World!", N: 1, F: 3.14 }
}

func injectedMessage() string {
    wire.Build(
        provideFoo,
        getS)
    return ""
}

使用 wire.FieldsOf()

func injectedMessage() string {
    wire.Build(
        provideFoo,
        wire.FieldsOf(new(Foo), "S"))
    return ""
}

生成的依赖注入代码

func injectedMessage() string {
    foo := provideFoo()
    string2 := foo.S
    return string2
}

提供清理函数

func provideFile(log Logger, path Path) (*os.File, func(), error) {
    f, err := os.Open(string(path))
    if err != nil {
        return nil, nil, err
    }
    cleanup := func() {
        if err := f.Close(); err != nil {
            log.Log(err)
        }
    }
    return f, cleanup, nil
}
  • 这个清理函数是闭包的,供外部调用

  • 如果 Provider 创建了需要清理的值(例如关闭文件),那么它可以返回一个清理函数来清理资源。注入器将使用它向调用者返回聚合清理函数,或者在注入器实现中稍后调用的提供程序返回错误时清理资源。

  • 清理函数保证在此 Provider 的任何依赖的清理函数之前被调用,必须具有签名func()

不 return 最终生成的对象

在依赖注入声明函数中,会返回一个无用对象,因为最终生成的依赖注入代码中,返回的不是你定义的那个对象。那怎样可以不写这个,但又不会编译错误呢?答案是抛异常。

func injectFoo() Foo {
    panic(wire.Build(/* ... */))
}

最佳实践

Options Structs

如果一个对象依赖太对对象,Provider 的方法签名就很长。当然可以使用 Struct Provider,不过这样就不能做一些初始化操作(以前写在 Provider 里的)。封装参数可以解决这个问题。

type Options struct {
    // Messages is the set of recommended greetings.
    Messages []Message
    // Writer is the location to send greetings. nil goes to stdout.
    Writer io.Writer
}

func NewGreeter(ctx context.Context, opts *Options) (*Greeter, error) {
    // ...
}

var GreeterSet = wire.NewSet(wire.Struct(new(Options), "*"), NewGreeter)
作者:Yuyy
博客:https://yuyy.info

评论

  1. Yuyy
    博主
    Linux Chrome
    已编辑
    2年前
    2022-4-20 14:52:22

    项目里遇到个有趣的问题,main函数里需要初始化grpc,并且还要执行一个依赖的方法,也就是想通过wire构建依赖的函数返回多个对象(一个是grpc,一个是需要后台运行函数的那个依赖对象)。
    查了好久都没查到有用的信息,最终搜索英文才查到了官方回复

    This behavior is by design. If you want to initialize multiple services in a provider function, create a struct with fields for the services and have the provider function return that.

    这不正是龙哥跟我说的解决办法吗,我两都认为这样不是很好😅

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇