Wire:Go最优雅的依赖注入工具

Wire:Go最优雅的依赖注入工具

导语

“成熟的工具,要学会自己写代码”。本文介绍了 Go 依赖注入工具 [[Wire]] 及其使用方法,以及在实践中积累的各种运用技巧。当代码达到一定规模后,[[Wire]] 在组件解耦、开发效率、可维护性上都能发挥很大的作用,尤其在大仓场景。

依赖注入

当项目变得越来越大,代码中的组件也越来越多:各种数据库、中间件的客户端连接,分层设计中的各种库表 repositories 实例、services 实例……

这时为了代码的可维护性,应该避免组件之间的耦合。具体的做法可以遵守一个重要的设计准则:所有依赖应该在组件初始化时传递给它,这就是依赖注入(Dependency injection)。

Dependency injection is a standard technique for producing flexible and loosely coupled code, by explicitly providing components with all of the dependencies they need to work.

– Go 官方博客

下面是个简单的例子,所有组件 Message、Greeter、Event 自身的依赖都在初始化的时候获得。

1

2

3

4

5

6

7

func main() {

message := NewMessage()

greeter := NewGreeter(message)

event := NewEvent(greeter)

event.Start()

}

Wire 介绍

当项目中实例依赖(组件)的数量越来越多,如果还是人工手动编写初始化代码和维护组件之间依赖关系的话,会是一件非常繁琐的事情,而且在大仓中尤其明显。因此,社区里已经有了不少的依赖注入框架。

除了来自 Google 的 Wire 以外,还有 Dig(Uber) 、Inject(Facebook)。其中 Dig 和 Inject 都是基于 Golang 的 Reflection 来实现的。这不仅对性能产生影响,而且依赖注入的机制对使用者不透明,非常的“黑盒”。

Clear is better than clever ,Reflection is never clear.

— Rob Pike

相比之下,Wire 完全基于代码生成。在开发阶段,wire 会自动生成组件的初始化代码,生成代码人类可读,可以提交仓库,也可以正常编译。因此 Wire 的依赖注入非常透明,也不会带来运行阶段的任何性能损耗。

上手介绍

这里快速介绍一下 Wire 的使用方法

第一步:下载安装 Wire

下载安装 wire 命令行工具

1

go install github.com/google/wire/cmd/wire@latest

第二步:创建 wire.go 文件

在生成代码之前,我们先声明各个组件的依赖关系和初始化顺序。在应用入口创建一个 wire.go 文件。

cmd/web/wire.go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

// +build wireinject

package main

import "..." // 简化示例

var ProviderSet = wire.NewSet(

configs.Get,

databases.New,

repositories.NewUser,

services.NewUser,

NewApp,

)

func CreateApp() (*App, error) {

wire.Build(ProviderSet)

return nil, nil

}

这个文件不会参与编译,只是为了告诉 Wire 各个组件的依赖关系,以及期望的生成结果。在这个文件:我们期望 Wire 生成一个返回 App 实例或 error 的 CreateApp 函数,App 实例初始化所需要的全部依赖都由 ProviderSet 这个组件列表提供,而 ProviderSet 声明了所有可能需要的组件的获取/初始化方法,也暗示组件之间的依赖顺序。

组件的获取/初始化方法,在 Wire 中叫做“组件的 provider”

还有几点需要注意:

文件开头必须带上 // +build wireinject 和随后的空行,否则会影响编译

在这个文件中,编辑器和 IDE 可能无法提供代码提示,但没关系,稍后会介绍如何解决这个问题

其中 CreateApp 的返回(两个 nil)没有任何意义,只是为了兼容 Go 语法。

第三步:生成初始化代码

命令行执行 wire ./...,然后就能得到下面这个自动生成的代码文件。

cmd/web/wire_gen.go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire

//go:build !wireinject

// +build !wireinject

package main

import "..." // 简化示例

func CreateApp() (*App, error) {

conf, err := configs.Get()

if err != nil {

return nil, err

}

db, err := databases.New(conf)

if err != nil {

return nil, err

}

userRepo, err := repositories.NewUser(db)

if err != nil {

return nil, err

}

userSvc, err := services.NewUser(userRepo)

if err != nil {

return nil, err

}

app, err := NewApp(userSvc)

if err != nil {

return nil, err

}

return app, nil

}

第四步:使用初始化代码

Wire 已经帮我们生成了真正的 CreateApp 初始化方法,现在可以直接使用它。

cmd/web/main.go

1

2

3

4

5

// main.go

func main() {

app := CreateApp()

app.Run()

}

使用技巧

组件按需加载

Wire 有个优雅的特点,不管在 wire.Build 中传入了多少个组件的 provider,Wire 始终只会按照实际需要来初始化组件,所有不需要的组件都不会生成相应的初始化代码。

因此,我们在使用时可以尽可能地提供更多的 provider,把挑选组件的工作交给 Wire。这样我们在开发时不管引用新组件、还是弃用老组件,都不需要修改初始化步骤的代码 wire.go。

比如,可以把 services 层中所有的实例构造器都提供出去。

pkg/services/wire.go

1

2

3

4

package services

// 提供了所有 service 的实例构造器

var ProviderSet = wire.NewSet(NewUserService, NewFeedService, NewSearchService, NewBannerService)

在初始化中,尽可能地引用所有可能需要的组件 provider。

cmd/web/wire.go

1

2

3

4

5

6

7

8

9

10

11

12

var ProviderSet = wire.NewSet(

configs.ProviderSet,

databases.ProviderSet,

repositories.ProviderSet,

services.ProviderSet, // 引用了所有 service 的实例构造器

NewApp,

)

func CreateApp() (*App, error) {

wire.Build(ProviderSet) // wire 会按照实际需要,选择性地进行初始化

return nil, nil

}

在后续开发中,如果需要引用新组件,只需要加到参数里即可。Wire 会任劳任怨地按照实际需要,生成需要的组件的初始化代码。

1

2

func NewApp(user *UserService, banner *BannerService) {

}

即使 Wire 找不到组件的 provider,也会提前在编译阶段报错,不会在线上运行阶段出现问题。

wire: cmd/api/wire.go:23:1: inject CreateApp: no provider found for *io.WriteCloser

编辑器与 IDE 的辅助配置

因为 wire.go 文件中加了这行注释,Go 在编译时会跳过这个文件,但也因此会影响编辑器和 IDE 的代码提示。当你在编辑 wire.go 文件时,常见的编辑器和 IDE 都无法正常地提供代码补全和错误提示功能。

1

// +build wireinject

但这个问题很容易解决。找到 IDE/编辑器的 Go 环境配置,在 Go Build Flags 中添加这个参数 -tags=wireinject 就可以了。

这个配置可以让编辑器和 IDE 正常地为 wire.go 文件提供代码补全和错误提示功能,开发体验提高不只一个数量级~

多个同类型组件的冲突问题

这个问题比较少见,但项目大了总是容易遇到。

Wire 通过 provider 的参数与返回类型,来判断组件的依赖关系。有时候,依赖网络中可能出现同类型的不同组件,这时 Wire 无法正确判断依赖关系,会直接报错。

provider has multiple parameters of type ...

比如下面这个 provider,依赖的 MySQL 和 PostgreSQL 客户端实例的类型是完全相同的(都是 *gorm.DB),这时 Wire 无法根据类型正确地判断依赖关系,生成代码时会直接报错。

1

2

3

// 这个 service 同时使用了 mysql 和 pg 中的数据,但是两个组件的类型是相同的

func NewService(mysql *gorm.DB, pg *gorm.DB) *Service {

}

解决的方法也比较简单,只需要做一层类型的包装。

1

2

3

4

5

6

7

8

9

10

type Mysql gorm.DB

type Pg gorm.DB

// 在参数中用类型别名进行区分

func ProviderSerivce(mysql *Mysql, pg *Pg) *Service {

// 函数内再转回原来的类型

r1 := (*gorm.DB)(mysql)

r2 := (*gorm.DB)(pg)

return NewService(r1, r2)

}

然后用 ProviderSerivce 代替 NewService 即可。

1

2

3

4

5

wire.Build(

ProviderMysql, // func() *Mysql

ProviderPg, // func() *Pg

ProviderSerivce, // func(mysql *Mysql, pg *Pg) *Service

)

自动生成构造函数

当项目中充当抽象类的结构体越来越多,手动编写和维护结构体的构造函数,也是一件非常繁琐的事情。如果结构体中新增了一个指针类型的成员、却忘记更新构造函数,甚至还会引起线上 panic。

1

2

3

4

5

6

7

8

9

10

11

type Service struct {

repo *Repository

logger *zap.Logger // 添加这个成员后,忘记更新构造函数了

}

func NewService(repo *Repository) *Service {

// 缺失 logger,可能在线上出现空指针错误

return &Service {

repo: repo,

}

}

像这种繁琐、重复、容易出错的工作,就应该交给自动工具来完成。这里我毛遂自荐一个自动工具 newc(意为 “New Construtor”),它可以自动生成与更新结构体的构造函数代码。

使用方法非常简单,只需要给结构体添加这行注释。

//go:generate go run github.com/Bin-Huang/[email protected]

比如这样:

1

2

3

4

5

6

7

// My User Service

//go:generate go run github.com/Bin-Huang/[email protected]

type UserService struct {

baseService

userRepository *repositories.UserRepository

proRepository *repositories.ProRepository

}

然后命令行执行 go generate ./... 即可获得构造函数代码:

constructor_gen.go

1

2

3

4

5

6

7

8

// NewUserService Create a new UserService

func NewUserService(baseService baseService, userRepository *repositories.UserRepository, proRepository *repositories.ProRepository) *UserService {

return &UserService{

baseService: baseService,

userRepository: userRepository,

proRepository: proRepository,

}

}

这个工具和 Wire 搭配使用,开发体验非常好。要使用新组件时,直接在结构体中添加成员就好了,不需要手动更新构造函数,也不需要考虑初始化的问题,所有重复的工作都交给自动工具(Wire 和 Newc)来完成。线下推荐过的同学,用过都说好。

当然这个工具也一定有考虑不周的情况,很期待大家的反馈和建议。

Don’t repeat yourself

“DRY”

总结

Wire 可以完美地解决依赖注入的问题,但它不是一个框架,它没有”魔法“,也不是黑盒。它只是一个命令行工具,它根据实际需要,自动生成了各个组件的初始化代码。然后问题就解决了,没有额外的复杂性,没有运行的性能损耗。

Wire 和 [[Golang]] 的气质如出一辙,简单、直接、实用主义,不愧是 Go 最优雅的依赖注入工具!

Keep it simple stupid

“K.I.S.S”

相关推荐

演了10次主角依旧不红 却娶了貌美女星
365bet足球即时比分网

演了10次主角依旧不红 却娶了貌美女星

📅 08-09 ⭐ 2241
绝地求生鼠标选择指南:提升游戏体验的关键要素
365bet足球即时比分网

绝地求生鼠标选择指南:提升游戏体验的关键要素

📅 09-24 ⭐ 485
《天涯明月刀手游》砭石获得方法
365bet足球即时比分网

《天涯明月刀手游》砭石获得方法

📅 08-20 ⭐ 5727
iPhone 变砖不用愁,四大修复方法全解析,帮你快速救回设备
起诉的钱到了法院多久会打到你账户
必发365手机版下载

起诉的钱到了法院多久会打到你账户

📅 08-04 ⭐ 1149
三星G7508Q/Galaxy Mega 2
365bet足球即时比分网

三星G7508Q/Galaxy Mega 2

📅 08-10 ⭐ 2275
推荐阅读 ❤️