go 是否需要依赖注入库曾经是一个饱受争议的话题。实际上是否需要依赖注入,取决于编程风格。依赖注入是一种编程模式。比较适合面向对象编程,在函数式编程中则不需要。go 是一门支持多范式编程的语言,所以在使用面向对象的大型项目中,还是建议按照实际情况判断是否应该使用依赖注入模式。
依赖注入实现的是一件很小的事情,所以单纯实现依赖注入的软件包称不上框架,而只能被称为库。 目前主流的 golang 库非常多,比如 uber 开源的 dig、elliotchance 开源的 dingo、sarulabs 开源的 di、google 开源的 wire 和 facebook 开源的 inject 等等。 目前最受欢迎的是 dig 和 wire,这篇文章主要介绍 dig 的用法。
创建容器
容器用来管理依赖。
注入依赖
调用容器的 Provide 方法,传入一个工厂函数,容器会自动调用该工厂函数创建依赖,并保存到 container 中。
注入的依赖会被 dig 所管理,每种类型的对象只会被创建一次,可以理解为单例。如果再注入同一类型的依赖,工厂函数则不会被执行。
使用依赖
如果需要使用依赖,使用 container 的 Invoke 方法。并在函数的形式参数中定义参数,container 会自动把单例注入。
当某个函数所需要的依赖过多时,可以将参数以对象的方式获取。 假设启动一个服务,需要 ServerConfig、MySQL、Redis、Mongodb 等参数。
此时代码可读性会变差,通过 dig.In 将 InitHttpServer 所依赖的 4 个依赖打包成一个对象。
和参数对象类似,如果一个工厂函数返回了多个依赖时,会有相同的问题。不过这种情况比较少见。 假设有个函数返回了启动 InitHttpServer 所需要的所有依赖。
解决这个现象的方式和 dig.In 类似,还有一个 dig.Out,用法一致。
如果在 Provide 的工厂函数或者 Invoke 的函数中所需要的依赖不存在,dig 会抛出异常。 假设 container 中没有 Mongo.Config 类型的依赖,那么就会抛出异常。
注入命名依赖
由于默认是单例的,如果需要两个相同类型的实例怎么办?比如现在需要两台 Mongodb 客户端。 dig 提供了对象命名功能,在调用 Provide 时传入第二个参数就可以进行区分。
除了传递 dig.Name 参数以外,如果使用了结果对象的话,可以通过设置 name 标签来实现,效果一致。
使用命名依赖
不论是 Provide 还是Invoke,都只能通过参数对象的方式使用命名依赖。使用方式是通过 tag,和注入时一致。
注意事项
嵌套 dig.In 的结构体的所有字段必须都在 container 中存在,否则 Invoke 中传入的函数不会被调用。 错误示例:
除了给依赖命名,还可以给依赖分组,相同类型的依赖会放入同一个切片中。 不过分组后就不能通过命名的方式访问依赖了,也就是说命名和组同时只能采用一种方式。 还是上面那个例子。
使用参数
使用结果对象
使用组
组只能通过对象参数的方式使用。
注意事项
和命名依赖相似,嵌套 dig.In 的结构体所有带有 gruop 标签的字段必须都在 container 中至少存在一个,否则 Invoke 中传入的函数不会被调用。 除此之外,group 返回的切片不保证注入时的依赖顺序,也就是说依赖切片是无序的。
到现在已经学完了 dig 所有的 API,下面稍微实战一下,通过 dig 的依赖组启动多个服务。
运行该文件,可以通过访问 http://127.0.0.1:8199、http://127.0.0.1:8299、http://127.0.0.1:8399 查看效果。 上面的示例使用命名依赖并不合适,但是为了演示 API 的实际使用,所以使用了命名依赖。 没有哪种 API 是最好的。在实际开发中,根据具体业务,使用最适合场景的 API,灵活运用即可。
pak dig pkg.go.dev