uber fx
fx 是 uber 开源的一款依赖注入框架,依赖注入这个名词对我来说一直是个很奇怪的存在(不了解 Java ),小项目引入依赖注入完全没必要啊,凭空提高复杂度(逃
fx 的作用是解决了golang项目中坐落各个包的全局变量,以及数不清的 init 函数。
这里也作为我学习的记录,分享一下我对 fx 的理解,包括一些源码的分析。
函数签名
这里列出主要会用到的函数与方法,并对其作用作出简要解释。
1 | func New(opts ...Option) *App |
其中 New 函数没什么好说的,它根据传入的 opts 构建 fx.App。
下面的四个函数的返回类型都是 Option,也就是 New 函数的入参。
- Provide
该函数传入的参数是构造函数 也就是 类似func NewC(A, B ...) C的函数 ,构造 C 需要依赖于 A,B … - Supply
函数传入的参数是已经构造完毕的值(value),也就是说Provide(NewC)→Supply(C), 其中 C = NewC(…)
当其他构造函数依赖于类型C时,不通过调用 NewC 生成,而是直接使用提供的 C。 - Populate
在New函数外部,我们先var了一个 C,并且通过Provide注入 C 的构造函数,那么外部通过植入的变量C,在初始化完成后通过Provide的构造函数完成构造。这样就可以在New函数外部使用这个经过构造的变量。 - Invoke
直接贴注释registers functions that are executed eagerly on application start
它注册一些在app启动时需要执行的函数,被注册的 func 的入参,通过 Provide 注入的构造函数生成。
需要注意的一点是 Invoke 注册的函数的运行是有顺序的,而 Provide 注入的构造函数并没有顺序,后面会更详细的分析。
1 | type fx.App struct {...} |
其中 Run 还是调用了 Start 。
使用
Provide、Populate、Invoke、Supply
在我们写 golang 项目的时候,经常会遇到要使用包内全局变量的,通过 import 其他包,来使用包内的全局变量。
1 | package modx |
我们可能会遇到这种情况
1 | func NewA() TypeA |
当我们需要一个 TypeC 时, 需要按照顺序手动构造 TypeA, TypeB,然后在构造 TypeC。
使用fx后
1 | func main() { |
我们将各个构造函数通过 Provide 函数注入到 fx.App 后,fx就会帮我们管理构造函数的调用,并且这种调用是 lazy的,即当某一个构造函数不存在被依赖时,那么它是不会被调用的。并且,这些构造函数被调用后构造的变量,是会被缓存的,所以当其他函数存在多个对其依赖时,只会被执行一次,之后都将直接返回第一构造的变量。
所以,我们使用 Provide 向fx注入构造函数时,注入顺序并不重要。
在这个例子里,我们 Populate 了一个TypeC 指针的外部变量,那么fx就会去调用 func NewC(),然后一次调用其依赖。
需要注意的是 Populate(targets …interface{}) 中传入的targets必须得是目标类型TypeX的指针类型 *TypeX,哪怕 TypeX 本身就是指针类型
同理,当我们不是Populate变量,而是 Invoke 一些函数,比如初始化函数,这些函数同样依赖于其余类型,那么fx就会去寻找对应的依赖的构造函数。
1 | func main() { |
!
所以在这里要再重点提一件事,所有的注入的构造函数都需要 Invoke or Populate 来 “激活链路”
在这里举一个我遇到的问题。
1 | type Server Struct { |
我有这么一个结构体,它依赖于 *HttpServer ,我在 NewServer 中添加了 hook(后面会讲),通过 Server 启动 http 服务。
然后我只使用 Invoke 添加了一个 AddRouter 来给 *HttpServer 添加路由。
当我 使用 fx.Run() 的时候,并没有看到终端打印 http 服务启动的信息。
其实就是作为入口的 AddRouter 只依赖了 *HttpServer,那么它只会去调用 NewHttpServer ,并没有执行 NewServer, 而我需要通过 Server 来注册 hook,启动 http 服务。
Supply 就略过不讲了,上一节已经足够了。
Run、Start、Lifecycle
这部分是比较复杂的部分,一般来说,像跑一个http服务,你可以只使用 Provide & Invoke & Populate 来完成依赖注入,然后使用 Populate 植入在 fx.New 里的外部变量,来启动http服务,比如我将一个 http.Server 植入,那么在 fx.New 结束后,我就可以拿着完成初始化的 http.Server 来启动 http 服务。
而 Run、Start、LifeCycle 主要涉及长期运行的协程,这块直接结合源码分析吧
我们看一下 fx.Lifecycle,它是 fx.App 的一个字段,构造函数的依赖用到它时,使用的就是 fx.New() 构造的 fx.App 的该字段。
下面这个是官方文档给出的例子,它在构造 ServeMux 的时注册了服务启动函数。
1 | func NewMux(lc fx.Lifecycle, logger *log.Logger) *http.ServeMux { |
我们需要向 Lifecyle 注册勾子,主要是添加一些需要在 fx.App 启动和关闭时需要执行的操作。
接下来我们再深入看看 Lifecycle.Start (主要看# 后的注释)
1 | func (l *Lifecycle) Start(ctx context.Context) error { |
1 | func (l *Lifecycle) runStartHook(ctx context.Context, hook Hook) (runtime time.Duration, err error) { |
我们可以看到,我们对于 hook 的调用是同步的,所以 hook 的执行不能消耗太多时间,可以通过在 hook 里开启新协程的方法来异步执行,就像官方文档给的例子一样。
搞明白了 Lifecycle 的作用,那么就可以深入了解一下 app.Start 。
1 | func (app *App) Start(ctx context.Context) (err error) { |
这里的callback是下面的 start 方法,它调用了 Lifecycle 的 Start 方法,作用上面讲了。
1 | func (app *App) start(ctx context.Context) error { |
来看 Run 和 Start 的关系
1 | func (app *App) Run() { |
我们可以看到,Run 方法调用了 Start(context.Context), 并监听退出信号(这里的done = app.Done() ,which 返回一个接收系统退出信号的双向通道。)
sig := <-done 主线程会被阻塞在这一行代码,直到收到系统的退出信号。
那么我们可以看到 Run 方法的意义其实就是为 Start & Stop 提供了一个包含默认超时时间的 context,用于 Start & Stop 的控制。
我们完全可以跳过 Run ,直接调用 Start ,这样可以自定义超时时间,这样就需要我们手动阻塞主线程,然后最后再调用 Stop 来执行 OnStop 的 hook。