前言

在参与 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
# 项目中添加依赖
go get -u github.com/google/wire

# 全局安装 Wire 的命令行程序
go install github.com/google/wire/cmd/[email protected]
wire
Wire 进行依赖注入的过程

当然,调用之前需要我们先进行完下面的步骤。

差异

传统依赖注入方法

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

此时编译执行当前目录我们会得到如下结果:

ClientFooBarClient

使用 Wire

service/wire.gomain.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()
}
ClientFooBarnilpanic: runtime error: invalid memory address or nil pointer dereference
wire ./...
wire ./...

# 或
go run github.com/google/wire/cmd/[email protected] ./...
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.goservice/wire_gen.goBuildClient() *Client
// 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.goservice/wire_gen.goservice/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 和其基本使用方法。