Wire 是由 Google 开源的 Go 语言依赖注入工具,通过代码生成的形式实现依赖的自动组装。大型项目的依赖繁杂,导致初始化流程长,代码可读性和维护性较差。同时,手动维护大量依赖的初始化代码,枯燥又耗时。Wire 可以根据组件的类型,分析出依赖顺序,自动生成初始化代码,简化开发工作。
快速上手
先看个具体的例子,该例子可以分成 3 层,按照依赖关系由低到高来看,分别是 Redis、Service 和 Handler。
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 34 35 36 37 38 39
| package main
import ( "github.com/alicebob/miniredis" "github.com/go-redis/redis/v8" )
type RedisClient struct { cli *redis.Client }
func NewRedisClient() *RedisClient { s, _ := miniredis.Run() addr := s.Addr() cli := redis.NewClient(&redis.Options{ Addr: addr, }) return &RedisClient{cli: cli} }
type UserService struct { database *RedisClient }
func NewUserService(database *RedisClient) *UserService { return &UserService{ database: database, } }
type Handler struct { userService *UserService }
func NewHandler(userSrv *UserService) *Handler { return &Handler{ userService: userSrv, } }
|
假如我们不使用 Wire 工具,直接手动写初始化代码。逐步调用每个的构建函数,最后组装得到 Handler 实例。依赖简单的情况下,手写也能轻松搞定,也推荐直接手写。但是如果依赖数量多,就很麻烦了,更推荐使用工具来完成。
1 2 3
| redisClient := NewRedisClient() userSvr := NewUserService(redisClient) NewHandler(userSvr)
|
现在我们使用 Wire 来自动生成初始化代码,首先按照文档安装命令行工具。
1
| go install github.com/google/wire/cmd/wire@latest
|
然后在 main 文件同目录下,新建 wire.go 文件,文件内容如下。注意加上编译条件 wireinject,防止后续生成的 InitHandler 函数与当前函数同名。
返回值类型必须是最终构建对象类型,实际返回的值可以不用关心,只需要关心返回参数的类型。
函数内需要调用 wire.Build
函数,而且需要传入所有依赖的构建函数。
1 2 3 4 5 6 7 8 9 10
|
package main
import "github.com/google/wire"
func InitHandler() *Handler { wire.Build(NewRedisClient, NewUserService, NewHandler) return &Handler{} }
|
接着在目录下,执行 wire
命令,预期会生成 wire_gen.go 文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
package main
func InitHandler() *Handler { redisClient := NewRedisClient() userService := NewUserService(redisClient) handler := NewHandler(userService) return handler }
|
对比上面手动写的初始化代码,完全一样,而且生成的代码没有引入额外依赖。生成的 InitHandler 函数实现了依赖注入关系,我们直接调用该函数,就可以避免自己手动实现绑定关系。
概念理解
在 Wire 中,有两个主要的概念,分别是 provider 和 injector。
provider(构造器)是组件的构建方法,可以生成组件。provider 的输入是组件所需的依赖,返回值是相应的组件。上述例子中 NewRedisClient、NewUserService 和 NewHandler 都是 provider。
injector(注入器)是根据依赖对 provider 打包的函数,它可以构建出最终的目标对象。injector 函数的返回值类型决定最终目标对象的类型,injector 函数内部必须调用 wire.Build
函数,并将所有依赖的 provider 作为输入参数,可以不需要考虑输入参数的顺序。上述例子中,wire.go 文件中的 InitHandler 函数是 injector。
常见用法
在上面小节中,已经通过一个实际的例子讲解了 Wire 的基本用法和基础概念,本小节主要讲一些更贴近实战的用法。
异常处理
实际编程中,provider 初始化组件时可能出现异常,所以我们需要通过返回 Error 判断 injector 是否成功构建最终的目标对象。我们修改上述的例子,如果初始化 miniredis 实例失败则返回错误。
1 2 3 4 5 6 7 8 9 10 11
| func NewRedisClient() (*RedisClient, error) { s, err := miniredis.Run() if err != nil { return nil, err } addr := s.Addr() cli := redis.NewClient(&redis.Options{ Addr: addr, }) return &RedisClient{cli: cli}, nil }
|
相应的也需要修改 injector 函数的结构,让它也支持返回错误。
1 2 3 4
| func InitHandler() (*Handler, error) { wire.Build(NewRedisClient, NewUserService, NewHandler) return &Handler{}, nil }
|
重新生成代码后,可以看到 InitHandler 函数已经支持了异常处理的能力了。
1 2 3 4 5 6 7 8 9
| func InitHandler() (*Handler, error) { redisClient, err := NewRedisClient() if err != nil { return nil, err } userService := NewUserService(redisClient) handler := NewHandler(userService) return handler, nil }
|
输入参数
Wire 不仅可以自定义 provider 参数,也可以定义 injector 参数。比如我们需要通过配置文件修改连接 Redis 的参数,可以定义 Config 结构体,并作为 injector 的输入参数。
1 2 3
| type Config struct { PoolSize int }
|
修改 NewRedisClient 函数的输入参数为 Config 类型。
1 2 3 4 5 6 7 8 9 10 11 12
| func NewRedisClient(conf Config) (*RedisClient, error) { s, err := miniredis.Run() if err != nil { return nil, err } addr := s.Addr() cli := redis.NewClient(&redis.Options{ Addr: addr, PoolSize: conf.PoolSize, }) return &RedisClient{cli: cli}, nil }
|
同时也需要修改 InitHandler 函数的输入参数类型。
1 2 3 4
| func InitHandler(conf Config) (*Handler, error) { wire.Build(NewRedisClient, NewUserService, NewHandler) return &Handler{}, nil }
|
重新生成后,InitHandler 函数就具备自定义输入参数的能力。
1 2 3 4 5 6 7 8 9
| func InitHandler(conf Config) (*Handler, error) { redisClient, err := NewRedisClient(conf) if err != nil { return nil, err } userService := NewUserService(redisClient) handler := NewHandler(userService) return handler, nil }
|
高级特性
至此,已经介绍完了 Wire 的常用操作,下面主要介绍一些进阶操作。
接口绑定
Wire 是通过类型来解析依赖关系的,如果 provider 的输入参数类型为接口时,那么要求前一个 provider 的返回值也必须要是相同类型的接口。但是返回接口的写法是不太地道,Go 更推荐返回具体的类型。所以可以让前一个 provider 返回具体类型,然后手动将具体的类型绑定到接口。
Accept interfaces return concrete types
我们对 RedisClient 增加 RedisItf 接口,NewRedisClient 函数保持不变,仍然返回 RedisClient 类型。NewUserService 使用接口 RedisItf 作为输入参数。
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
| type RedisItf interface { }
type RedisClient struct { cli *redis.Client }
func NewRedisClient(conf Config) (*RedisClient, error) { s, err := miniredis.Run() if err != nil { return nil, err } addr := s.Addr() cli := redis.NewClient(&redis.Options{ Addr: addr, PoolSize: conf.PoolSize, }) return &RedisClient{cli: cli}, nil }
type UserService struct { database RedisItf }
func NewUserService(database RedisItf) *UserService { return &UserService{ database: database, } }
|
修改 InitHandler 的函数体,显式地将 RedisItf 与 RedisClient 绑定。
1 2 3 4
| func InitHandler(conf Config) (*Handler, error) { wire.Build(NewRedisClient, NewUserService, NewHandler, wire.Bind(new(RedisItf), new(*RedisClient))) return &Handler{}, nil }
|
从生成的代码可以看出,NewUserService 实际的参数是 redisClient,也就是 RedisItf 的具体实现。
1 2 3 4 5 6 7 8 9
| func InitHandler(conf Config) (*Handler, error) { redisClient, err := NewRedisClient(conf) if err != nil { return nil, err } userService := NewUserService(redisClient) handler := NewHandler(userService) return handler, nil }
|
属性注入
我们发现有些 provider 的工作非常简单,只是创建对象实例,然后给属性赋值,没有其他初始化逻辑,比如上述例子中的 NewHandler 函数。为了减少写这种重复、低级的 provider 代码,Wire 提供属性自动注入功能。
通过 wire.Struct
函数就可以指定结构体的哪些属性需要绑定。比如这里 Handler 的 userService 需要进行属性绑定。
1 2 3 4 5 6 7 8 9
| func InitHandler(conf Config) (*Handler, error) { wire.Build( NewRedisClient, NewUserService, wire.Bind(new(RedisItf), new(*RedisClient)), wire.Struct(new(Handler), "userService"), ) return &Handler{}, nil }
|
如果需要绑定 Handler 的所有属性,可以使用 *
匹配所有属性。如果想过滤特定属性,可以给 Handler 的属性加上 wire:"-"
的 tag。
1
| wire.Struct(new(Handler), "*")
|
重新生成的代码可以完全替代 NewHandler 函数。
1 2 3 4 5 6 7 8 9 10 11
| func InitHandler(conf Config) (*Handler, error) { redisClient, err := NewRedisClient(conf) if err != nil { return nil, err } userService := NewUserService(redisClient) handler := &Handler{ userService: userService, } return handler, nil }
|
清理函数
如果 provider 创建的对象需要进行关闭或者回收,比如关闭文件描述符,那么就需要使用清理函数。Wire 可以支持返回闭包函数,遇到 Error 时,执行前一个 provider 返回的清理函数。我们这里将 NewRedisClient 的第二个返回参数改成闭包函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| func NewRedisClient(conf Config) (*RedisClient, func(), error) { s, err := miniredis.Run() if err != nil { return nil, nil, err } addr := s.Addr() cli := redis.NewClient(&redis.Options{ Addr: addr, PoolSize: conf.PoolSize, }) cleanup := func() { s.Close() } return &RedisClient{cli: cli}, cleanup, nil }
|
同时将 NewUserService 支持返回错误。
1 2 3 4 5 6 7 8
| func NewUserService(database RedisItf) (*UserService, error) { if database == nil { return nil, errors.New("nil database") } return &UserService{ database: database, }, nil }
|
最后将 injector 的返回类型也支持清理函数。
1 2 3 4 5 6 7 8 9
| func InitHandler(conf Config) (*Handler, func(), error) { wire.Build( NewRedisClient, NewUserService, wire.Bind(new(RedisItf), new(*RedisClient)), wire.Struct(new(Handler), "*"), ) return &Handler{}, nil, nil }
|
可以看到现在生成的代码中,如果 NewUserService 失败时,会运行 NewRedisClient 返回的清理函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| func InitHandler(conf Config) (*Handler, func(), error) { redisClient, cleanup, err := NewRedisClient(conf) if err != nil { return nil, nil, err } userService, err := NewUserService(redisClient) if err != nil { cleanup() return nil, nil, err } handler := &Handler{ userService: userService, } return handler, func() { cleanup() }, nil }
|
类型重复
如果依赖链路上,出现两个相同类型的依赖,那么 Wire 将会报错,因为它已经无法判断正确的依赖关系。解决办法就是创建新的类型,然后将返回值强转成新的类型。
我们以 Handler 举例,现在 Handler 有两个 UserService 类型的属性,按照现在的定义,代码生成会报错失败。
1 2 3 4
| type Handler struct { userService *UserService userServiceV2 *UserService }
|
解决办法很简单,就是定义 UserServiceV2,并且增加对应的 provider。同时将 Handler 的第二个属性类型改成 UserServiceV2。
1 2 3 4 5 6 7 8 9 10 11
| type UserServiceV2 UserService
func NewUserServiceV2(database RedisItf) (*UserServiceV2, error) { us := &UserService{database: database} return (*UserServiceV2)(us), nil }
type Handler struct { userService *UserService userServiceV2 *UserServiceV2 }
|
别忘了把新的 provider 添加到 injector 中。
1 2 3 4 5 6 7 8 9 10
| func InitHandler(conf Config) (*Handler, func(), error) { wire.Build( NewRedisClient, NewUserService, NewUserServiceV2, wire.Bind(new(RedisItf), new(*RedisClient)), wire.Struct(new(Handler), "*"), ) return &Handler{}, nil, nil }
|
现在 Wire 就可以正确识别 Handler 的所有属性了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| func InitHandler(conf Config) (*Handler, func(), error) { redisClient, cleanup, err := NewRedisClient(conf) if err != nil { return nil, nil, err } userService, err := NewUserService(redisClient) if err != nil { cleanup() return nil, nil, err } userServiceV2, err := NewUserServiceV2(redisClient) if err != nil { cleanup() return nil, nil, err } handler := &Handler{ userService: userService, userServiceV2: userServiceV2, } return handler, func() { cleanup() }, nil }
|
全文总结
Wire 仅仅根据对象的类型来分析依赖关系和顺序,所以必须保证类型唯一、不重复,只有这样才不会产生歧义的构造关系。
Wire 主要由 provider 和 injector 两部分组成,按照返回值类型来区分 provider 的话,provider 只有 3 种写法。以 NewRedisClient 为例,3 种写法如下所示。
1 2 3
| NewRedisClient() *RedisClient NewRedisClient() (*RedisClient, error) NewRedisClient() (*RedisClient, func(), error)
|
本文列举的用法都是 Wire 实际编程中常用的操作,掌握之后可以解决大部分使用过程中的问题。更详细的使用建议参考官方文档。