精通Wire
wire是什么 ?
Wire是Go Cloud团队开发的Golang中的轻量级依赖注入工具。它会自动生成代码,然后在编译时注入依赖项。
依赖注入是保持软件“loose-coupling and easy to maintain(松耦合且易于维护)”的最重要的设计原则之一。
该原理已在所有类型的开发平台中广泛使用,并且有许多与之相关的出色工具。
在所有这些工具中,最著名的是Spring和Spring IOC,该框架的基础对其今天的主导地位起着决定性作用。
实际上,S.O.L.I.D软件开发原则中的“ D”是专门针对此主题的。
Wire与其它依赖注入工具有什么不同 ?
依赖注入是如此重要,以至于Golang社区中已经有很多解决方案,例如从Uber的dig和从Facebook的inject。它们都通过反射机制实现了运行时依赖项注入。
Go Cloud团队为什么要彻底改变方向?因为他们认为上述所有库都不符合Go的哲学:
Clear is better than clever ,Reflection is never clear. 清晰胜于巧妙,反射永远不会清晰。 — Rob Pike
作为代码生成工具,Wire可以生成源代码并在编译时实现依赖项注入。它不需要反射或Service Locators。如您将在后面看到的,Wire生成的代码与手工编写的代码没有区别。这种方法带来一些好处:
- 轻松调试。如果缺少任何依赖项,则在编译过程中将报告错误
- 由于不需要service locators,因此对命名没有特殊要求
- 避免依赖膨胀。生成的代码将仅导入您需要的依赖项,而运行时依赖项注入直到运行时才能识别未使用的依赖项。
- 依赖图静态存储在源代码中,这使得工具和可视化变得更加容易
有关设计Wire的详细折衷方法,请访问Go Blog。
尽管Wire的最新版本仅为v0.4.0,但它已经达到了团队设定的目标并且相当成熟。预计将来不会有重大变化。从团队的声明中可以看出:
It works well for the tasks it was designed to perform, and we prefer to keep it as simple as possible._ We’ll not be accepting new features at this time, but will gladly accept bug reports and fixes._ —Wire team
Getting Started
安装wire非常容易,只需运行
|
|
您将在$GOPATH/bin
上安装Wire命令行工具,确保$GOPATH/bin
在$PATH
中,然后可以在任何目录下运行wire。
在继续之前,我们需要在Wire中解释两个核心概念:Provider和Injector。
Provider:用于创建组件的普通函数。这些方法将所需的依赖项作为参数,创建一个组件并返回它。
组件可以是对象或函数,实际上它可以是任何类型。唯一的限制是:one type can only have a single provider in the entire dependency graph。因此,返回int
的提供程序不是一个好主意。在这种情况下,您可以通过定义类型别名来解决它。例如,首先定义type Category int
,然后让提供者返回Category
类型
以下是典型的provider示例:
|
|
实际上,通常将一组相关的提供程序放在一起并组织到ProviderSet中,以方便维护和切换。
|
|
Injector:由wire生成的函数,以依赖关系顺序调用提供程序。
为了生成injector,我们在wire.go
中定义了 the injector function signature (文件名不是必需的,但通常是这种情况)。然后调用wire.build
in the function body with provider as parameter (与顺序无关)。
由于wire.go
中的函数并未真正返回值,因此为了避免编译器错误,只需简单的用panic
包裹他们。不用担心运行时错误,因为它实际上不会执行,所以只是生成实际代码的提示。一个简单的wire.go示例:
|
|
完成此代码后,运行命令行wire
将生成文件wire_gen.go
,其中包含 injector function 的实现。 wire.go中的任何非注入器代码都将照原样复制到wire_gen.go(尽管在技术上允许,但不建议这样做)。生成的代码如下:
|
|
上面的代码有两个有趣的地方:
wire.go
的第一行//+build wireinject
,此构建标记可确保在常规编译期间忽略wire.go文件(因为在常规编译期间未指定wireinject标记)。与之相反,wire_gen.go
的第四行//+build! Wireinject
。这两套相对的构建标记可确保在任何情况下,wire.go和wire_gen.go文件只有一个生效,避免了编译错误“function UserLoader has been defined”- 自动生成的函数
UserLoader
包含错误处理。它几乎与手写代码相同。对于这样一个简单的初始化过程,手写是可以接受的,但是当组件的数量达到数十,数百甚至更多时,将显示出自动生成的优点。
有两种方法可以触发“生成”操作:go generate
或wire
。
前者仅在wire_gen.go已经存在的情况下才有效(因为wire_gen.go的第三行//go:generate wire
)。
虽然后者可以随时执行。后者支持更多参数来微调生成行为,因此建议始终使用wire
命令。
然后我们可以使用real injector,例如:
|
|
如果您不小心忘记了a certain provider,wire将报告特定错误,以帮助开发人员快速解决问题。例如,我们修改wire.go以删除NewDb
|
|
执行wire
命令,然后将报告一个显式错误:“no provider found for * example.Db”
|
|
同样,如果在wire.go
中编写了未使用的提供程序,则将出现明确的错误消息。
高级功能
在讨论基本用法之后,让我们看一下高级功能
Binding Interfaces
有时我们需要注入一个接口。有两种选择:
- 直接的方法是在provider中创建一个类,然后返回接口类型。但这不符合Go最佳实践。不建议
- 让provider返回类,然后在注入器中声明一个接口绑定,例如:
|
|
Struct Providers
有时我们不需要任何特定的初始化工作,我们只需创建一个struct实例,为指定的字段分配一个值,然后返回即可。当字段很多时,这种工作可能会很乏味。
|
|
在这种情况下,wire.Struct可以通过指定字段名称来注入字段:
|
|
如果要注入所有字段,则有一种更简化的方法:
|
|
如果要忽略结构中的某些字段,则可以修改结构定义:
|
|
然后NoInject
将被忽略。与 regular providers 相比,wire.Struct
提供了额外的灵活性:它可以适应指针和非指针类型,并根据需要自动调整生成的代码。
而wire.Struct
确实提供了一些便利。但这要求注入的字段必须是可公开访问的,这会导致结构公开可能会隐藏的细节。
幸运的是,可以通过上述“绑定接口”解决此问题。使用wire.Struct
构造一个对象并将类绑定到接口。至于如何在便利性和封装之间进行权衡,则取决于您的具体情况。
Binding Values
有时您需要将值绑定到字段。在这种情况下,您可以使用wire.Value
:
|
|
对于接口值,请使用wire.InterfaceValue
|
|
Use Fields of a Struct as Providers
有时我们需要使用结构的字段作为提供者,例如:
|
|
在这种情况下,您可以使用wire.FieldsOf
简化它,避免繁琐的提供程序定义:
|
|
与wire.Struct
类似,wire.FieldsOf
也可以自动适应指针/非指针注入请求
Cleanup functions
如前所述,如果provider and injector函数返回错误,Wire将自动处理它们。此外,Wire还具有另一种自动处理功能:cleanup functions。
所谓的cleanup function是指带有签名func()
的闭包。它从提供程序返回以确保可以清除在提供程序中分配的资源。
cleanup function的典型应用场景是文件资源和网络连接资源管理,例如:
|
|
上面的代码定义了两个提供程序,分别提供文件资源和网络连接资源。 wire.go
|
|
请注意,因为提供程序返回了a cleanup function,所以注入器函数还必须返回它,否则将发生错误
wire_gen.go
|
|
生成的代码中有两点值得注意:
- 当
ProvideNetConn
失败时将调用cleanup()
,以确保后续处理错误不会影响先前分配的资源的清除。 - 返回的最终闭包自动组合
cleanup2()
和cleanup()
。这意味着无论分配多少资源,只要调用过程成功,它们的清理工作都将在一个清理函数中进行处理。The caller of the injector will be responsible for the final cleanup
可以想象,有很多 cleaning functions 组合在一起时,手动处理以上两个问题非常繁琐且容易出错。 Wire的优势再次得到体现。
然后我们可以使用它:
|
|
请注意defer cleanup()
,它确保最终回收所有资源。