本文正在参加「金石计划」

为什么说IoC仅仅一种代码的指导思维

写在前面

和几个小伙伴在字节跳动第五届青训营后端组中,编写的Dousheng项目大作业获奖了。假如是Javer,肯定运用过Spring,那么应该知道IoC,但并不是Spring才能运用IoC。它是一种怎样编写代码的指导思维,比方咱们的Go Project中,一样运用到了这个思维。来看看咱们是怎样运用的吧。

首先附上项目地址:

  • 项目地址
  • 项目文档

青训营之旅结束了,我给朋友(简称小cher算了️‍♂️️‍♂️)看了我在我Go Project中完成的简易版IOC,完成思路如图所示:

花3分钟写的简易IoC,放在Golang的项目中太好用了~

什么?小cher说她看不懂!!!好吧,我的锅。那我来解说解说,什么是IoC?

注:本文是运用了Ioc的一些思维,完成了简易版的IoC容器,和IoC的原思维并不彻底一致。能够小马过河,因人而异~

一、什么是IOC

️‍♂️️‍♂️小cher说,你用一句话解说I一下IoC的中心思维:

一句话解说:将目标运用操控回转的办法,在容器中创立出Bean(项目的依靠目标),并且能够主动为每一个Bean注入所需的依靠。

再用一句话解说简易版IoC:将依靠目标示入容器中后,用操控回转的办法,从IoC容器中获取所需求的依靠。

我还补充了一句:

当然,一般从IoC中取出的依靠,也是为了注入给容器中的其它依靠。

️‍♂️️‍♂️这一下小cher就懵了啊:

什么是操控回转?什么又是依靠?注入之后怎样取出来勒?为什么会有IoC勒?

没联系,咱们渐渐来看,就聊聊天嘛~

(1)什么是依靠注入?

小cher啊,我先跟你说官方一点的啊:

1、官方一点的

DI(Dependency Injection)称为依靠注入。意思是:假如A实例依靠B实例,如下代码所示:

type A struct {
    // A目标依靠目标 B
    b B
}
type B struct {
    name string
}

在程序发动时的时分,会去初始化IoC容器,去初始化目标A的时分,扫描到它需求依靠目标B。IoC会从自身取出B,给A目标中的b字段赋值。

️‍♂️️‍♂️我说完这句话后:小cher说:

这个进程,假如给A赋值的时分,B目标还未初始化呢?那之后假如调用A.b的办法,不就相当于A.nil.xxx了嘛?

是的,所以咱们要求,在进行依靠注入的时分,必需求能够在容器中能找到被依靠的目标。


️‍♂️️‍♂️话音刚落,小cher加上了自己的考虑,现已了解大约的意思了,他说:那还有没有简略一点的描述?

2、简易版

简略说明了上面的那种解说,再来强化一下了解,由于咱们要完成的简易版,依靠注入指的是:

假如将程序刚开端发动时分为两个阶段

  • 阶段一:经过一定的手法,将依靠放入IoC对应的容器中
  • 阶段二:去初始化容器中的目标,也便是给容器中每一个目标的特点赋值。

也便是图中的这个进程:

花3分钟写的简易IoC,放在Golang的项目中太好用了~

️‍♂️️‍♂️图还没看完,小cher说,欧,本来这两个过程便是依靠注入的进程啊!!!这个懂了,可是我还有个疑问:你说经过一定的手法。详细指的是什么呢?

(2)怎样注入容器?

是啊,怎样经过一定的手法将依靠放入容器呢?这儿有几个可借鉴的手法:可所以装备文件、可所以注解、注释…

放下上面这句话,小cher啊,假如有一个盒子,让你把一个球放入一个盒子里,你会怎样放入呢?

️‍♂️️‍♂️我啊,我会大致会有两个思路吧:

  1. 自己手动放进去咯
  2. 跟我女朋友说,当我这个盒子打开的时分,她就把球主动放进去~

???满脸羡慕,有女朋友真幸福是吧~ 咳咳,先持续来看这个问题:

呀,还挺会考虑的勒。主线思路也便是主动放入和手动放入咯,可是咱们作为”高级“程序员,会手动操作吗?

所以,咱们注入容器的进程,就选择主动注册,那怎样注册呢?已然我这儿是Go ProjectGo言语的程序员围过来,其他言语围观,咱们来看一段 Go官方的Mysql驱动包 的代码(主要看标示的):

花3分钟写的简易IoC,放在Golang的项目中太好用了~

小cher,相信你猜出我想说什么了吧!

️‍♂️️‍♂️那我浅猜一下?你是不是想说:

在程序刚开端发动时,假如你的发动入口导入了对应的包,那么就会去加载那个包的一些东西。我在网上看到了一副图片,共享给你看看:

花3分钟写的简易IoC,放在Golang的项目中太好用了~

而运用匿名导入,是由于在导入包的当地不需求运用它,仅仅想去初始化那个包。你看看我说的对吗??

哇偶,能够啊,小cher你是懂我的!!!来,碰个杯我再接着你的思路往下说

咱们知道了这个原理,那么咱们就能够即将放入容器的操作写到init()函数里,比方下面两段伪代码:

package api
// 这个包有依靠需求放入IoC容器
// ...
func init() {
    // 放入IoC容器的逻辑
    ioc.DI(需求放入的依靠)
}

那么在发动程序的时分:

package main
import (
	_ "test/api" // 去初始化 api 包
)
func main() {
    // 发动程序 
    // ....
}

️‍♂️️‍♂️小cher看完了这两段代码,恍然大悟,本来是这样啊!!!那我想问问,说操控回转,操控怎样就回转了呢?

(2)操控怎样就回转了?

好的,小cher,看这个问题之前,你再回想一下,你在项目中一般是怎样运用依靠目标的?最好能够写点简略的代码给我看看

️‍♂️️‍♂️呐,你看

// 目标A
type A struct {
   id int
   b  *B // 这儿依靠 B 目标
}
// 目标B的结构函数
func NewB(name string) *B {
   return &B{name: name}
}
// 目标B
type B struct {
   name string
}
func main() {
   a := A{
      id: 20,
      b:  NewB("ciusyan"), // 自己操控B目标的初始化
   }
   fmt.Println(a.b.name)
}

是啊,你看看,你写的这个,你的A目标依靠B目标,你要是给A目标初始化。你需求自己去写初始化代码。这样我觉得费事的当地便是:

  1. 假如你这个类似的代码在100个当地用了,那么你就会写一百遍类似的代码。假如你的参数突然变化了,那么你又要到那用到的一百个当地修改代码
  2. 需求自己理清楚所依靠的目标

️‍♂️️‍♂️也是偶,那我能够封装一下啊!!!哎,不对,你不便是在运用IoC的思维封装吗

是的,咱们运用IoC的办法封装后(你先别管详细怎样封装的),初始化的操作,都被放入容器中了。需求运用目标的时分,直接从容器中取出来即可,比方:

func main() {
    // 从IOC中获取A目标
   a := ioc.Get("A")
    // 它是怎样初始化A和B的,咱们根本不需求关怀
   fmt.Println(a.b.name)
}

如上代码所示,咱们从IoC中取出了想要运用的目标A,它的内部是怎样初始化目标B的,咱们根本不必关怀,也不必顾虑,它到底是用阿猫、仍是阿狗来初始化的。

这下对B的操控权,不就回转了吗?从你自己操控,回转成了IoC容器帮你操控

再给你找一副图看看:

花3分钟写的简易IoC,放在Golang的项目中太好用了~

️‍♂️️‍♂️好吧,的确是,用IoC封装后,运用起来好便利啊,都不需求自己办理依靠的目标了!!!再看你开端给我看的图,如同清晰了许多

花3分钟写的简易IoC,放在Golang的项目中太好用了~

相信你看到这儿,你大约也知道为什么会出现IoC了,那咱们再来总结一下~

二、为什么会有IOC

一句话解说:便利办理项目的依靠的目标。

刚刚所述的,假如许多重复性很大的代码,这个点咱们不重复了叙述了。下面来看看,假如一个目标的依靠许多,那么你或许去理清这个目标的依靠,会很费事。

(1)没有IoC时

1、依靠联系紊乱

假如你的目标许多,目标间的依靠联系或许很难办理,你或许很难理清楚它们之间的联系。比方下图:

花3分钟写的简易IoC,放在Golang的项目中太好用了~

将上图的依靠联系转换为一段伪代码:

// 目标A
type A struct {
   id int
   b  *B // 这儿依靠 B 目标
   c  *C // 这儿依靠 C 目标
   d  *D // 这儿依靠 D 目标
   e  *E // 这儿依靠 E 目标
   f  *F // 这儿依靠 F 目标
   g  *G // 这儿依靠 G 目标
   // ....
}
func main() {
    // 创立G目标
    g := NewG()
    // 创立I目标,它依靠G
    i := NewI(g)
    // 创立D目标。它依靠I
    d := NewD(i)
    // 创立H目标,它依靠D
    h := NewH(d)
    // 创立目标A
    a := A{
          id: 20,
          b:  NewB(h), // 自己操控B目标的初始化,B依靠H
          c:  NewC(),  // 自己操控C目标的初始化
          d:  d, 	   // 自己操控D目标的初始化
          e:  NewE(),  // 自己操控E目标的初始化
          f:  NewF(),  // 自己操控F目标的初始化
          g:  g,       // 自己操控G目标的初始化
          // ....
       }
    // 运用A目标
    fmt.Println(a.id)
}

️‍♂️️‍♂️小cher看完了这段代码,笑道:之前目标的依靠没那么多,如同的确没多少感觉,看你这个依靠多了后,如同是有点费事勒

哈哈,没骗你吧,我这仍是随便画的图片,看起来还没那么紊乱,你或许仍是能理清楚,可是咱们再来看一种状况。

2、循环依靠问题

假如是在Go言语中,你每依靠一个目标,就需求导入对应的包,依靠联系紊乱或许还能够理清楚,可是假如依靠构成一个环状,你导入包的时分,或许会导致包循环导入 (import circle)的问题。

比方下面这段伪代码:

这是A包的目标,它依靠B包的目标

package A
import "circle/B"
type AImpl struct {
	b *B.BImpl
}

这是B包的目标,它依靠C包的目标

package B
import "circle/C"
type BImpl struct {
	c *C.CImpl
}

这是C包的目标,它依靠A包的目标

package C
import "circle/A"
type CImpl struct {
   a *A.AImpl
}

能够看到这三段代码的 import ,大致是这样的:A -> B,B -> C,C -> A,这样的联系,你运用其中任意一个目标,都会报错。比方这样运用:

package main
import "circle/A"
func main() {
   // 运用其中一个目标,都会报错
   _ = A.AImpl{}
}

假如你运行上面的代码,会导致形如下面这样的错误:

package circle
	imports circle/A
	imports circle/B
	imports circle/C
	imports circle/A: import cycle not allowed

再画一张图给你看看,上面的依靠联系是这样的,循环依靠了。

花3分钟写的简易IoC,放在Golang的项目中太好用了~

(2)有IoC之后

看完了上面的两个问题,我想,你多少或许都遇到过上面的问题吧!那来看看有IoC之后呢?

// 界说这些目标的时分,将这些目标全部注入IoC容器中
ioc.DI(A, B, C, D, E, F, G, H)
// 运用目标A
a := ioc.Get("A")

假如站在目标A维护者的视点来看,咱们将A目标放入容器中后,正确的给其注入所需的依靠。在外界运用者的眼里,就只需求 ioc.Get() 获取目标来运用即可。外界甚至都能够不必知道这儿面是怎样初始化的。这不就给运用者一个很大的便当了吗?由于不必花时间去收拾依靠联系了~

并且现在依靠都是由IoC办理的,创立者只需求将目标创立好,放入IoC中即可,这样的话,IoC在进行依靠注入的时分,都是从容器里边取依靠,根本不会有循环导入的问题了~

当然啊,并不是说没有IoC就不行啊,但若你还没有想到好的计划,无妨能够试试用IoC容器化的思维去封装一些东西?

小cher,聊到现在,相信你应该了解了IoC的一些中心概念,那咱们开端制造装依靠的容器,看看3分钟能不能写完吧!

三、开端实践了

(0)完成铺垫

一般会用许多种容器,来装不一样的依靠。

小cher,问你个问题,你家里的大衣柜,你不或许把一切衣物,一啪啦的全扔进去吧。

️‍♂️️‍♂️肯定不会啊,要不然到时分取的时分太费事了,能够分隔办理一下的

是我的话,我或许会把它分成许多个装衣服的容器:有装衬衫的、装领带的、装西装、装裤子…

哈哈哈,是的,已然你也这样以为。那咱们这儿也能够多做几个容器,用于装不一样的目标,分隔办理。就先来完成一个用来装Gin的HTTP服务目标的容器吧,其余的也是类似思路!

这儿以在Main办法中注册HTTP的Handler为例,比方现在在main中是这样运用的:

func main() {
    // 1、加载装备文件
    if err := conf.LoadConfigFromToml(configFile); err != nil {
        panic(err)
    }
    // 2、经过Gin发动HTTP服务
    g := gin.Default()
    // 3、注册对应服务的路由
    // 获取UserHandler目标,并且注册路由    
    userService := impl.NewUserServiceImpl()
    userApi := http.NewUserHttpHandler(userService) // UserHandler 依靠 UserService
    userApi.Registry(g)
    // 获取VideoHandler目标,并且注册路由
    videoService := impl.NewVideoServiceImpl()
    videoApi := http.NewVideoHttpHandler(videoService) // VideoHandler 依靠 VideoService
    videoApi.Registry(g)
    // 依此类推,注册其他服务的路由 ....
    // 4、发动监听TCP衔接
    g.Run(conf.C().App.HttpAddr())
}

这儿一定要看理解哟,由于咱们便是要对这一段代码做改善,这儿的逻辑其实也不难:

  1. 读取项目所需装备
  2. 将路由注册到Gin中去监听
  3. 发动HTTP服务,开端监听衔接

要点看第二步,咱们这儿假设就有两个模块,User 和 Video。咱们只注册了两次。假如有10个模块的Handler,那么这样的代码就需求写10遍,它们依靠的Service也需求获取十遍。

那咱们怎样封装呢?跟着我的思路

(1)初阶简易版别

已然这儿Handler目标需求咱们手动注入依靠的Service目标,那咱们能不能先把依靠的操控权,先回转呢?

1、树立IoC包

先树立一个IoC的包,这儿面装着依靠的两个Service目标

package ioc
import (
    "github.com/Go-To-Byte/DouSheng/apps/user"
    "github.com/Go-To-Byte/DouSheng/apps/video"
)
// IOC容器:办理一切服务的实例
var (
	UserService user.Service
	VideoService video.Service
        // .... 其他依靠
)

留意这儿都要用对应Service的接口欧, 我这儿就不赘述为什么要面向接口编程了~

2、修改Main的代码

那么咱们在main中,就能够这样改善那一段代码了,为了便利,我只保留咱们要封装的代码:

func main() {
    // 3、注册对应服务的路由
    // 将所需的依靠目标的完成,手动注入到IoC包中去。   
    ioc.UserService := impl.NewUserServiceImpl()
    ioc.VideoService := impl.NewVideoServiceImpl()
    userApi := http.NewUserHttpHandler()
    userApi.Registry(g)
    // 获取VideoHandler目标,并且注册路由
    videoService := impl.NewVideoServiceImpl()
    videoApi := http.NewVideoHttpHandler(videoService) // VideoHandler 依靠 VideoService
    videoApi.Registry(g)
    // 依此类推,注册其他服务的路由 ....
}

在这儿,咱们先将IoC中接口所需求的完成,给手动注入进去(给依靠目标赋值)

然后咱们直接新建一个目标即可,不需求手动传入这个依靠的Service目标了

3、修改结构函数

那么这两个Handler的初始化的结构办法,就得修改一下了:


// User Handler 的结构函数
func NewUserHttpHandler() *UserHandler {
    if ioc.UserService == nil {
		panic("IOC中依靠为空:UserService")
	}
    return &Handler{
            service: ioc.UserService, // 这儿依靠IoC中的UserService目标
    }
}
// Video Handler 的结构函数
func NewVideoHttpHandler() *VideoHandler {
    if ioc.VideoService == nil {
		panic("IOC中依靠为空:VideoService")
	}
    return &Handler{
            service: ioc.VideoService, // 这儿依靠IoC中的VideoService目标
    }
}

在结构函数中初始化的时分,直接从IoC中去获取依靠的Service目标。

4、初阶版总结

小cher看完这个初阶版别,心里有一万只。在初阶版,你或许看到我感觉什么都没做,可是你细心考虑一下,咱们的初阶版别

是不是在运用依靠前,将依靠注入进去了?需求运用的时分,是不是直接从IoC中取出来的?别着急,咱持续往下看看。

感觉也没问题,可是其实还有许多问题,比方:

  1. 需求手动注入依靠
  2. 操控并没有彻底回转
  3. 注入的依靠很随意
  4. 代码仍是不行精简

那咱们接着来改善改善,让它看起来更标准些吧~

(2)完好简易版别

1、标准IoC容器的目标的接口

先界说一组接口,想要将目标放入装Gin的HTTP目标的容器中,必需求完成这个接口。

// GinDependency Gin HTTP的服务实例想要注入此容器,有必要完成该接口
type GinDependency interface {
	// Init 怎样初始化注入此 IoC 的实例
	Init() error
	// Name 注入服务模块的称号
	Name() string
	// Registry 该模块所需求注册的路由
	Registry(r gin.IRoutes)
}

假如你将依靠放入了IoC中,你还得告诉它,要怎样初始化,所以需求完成Init()办法。每放入一个依靠,用什么来标识它,所以需求完成Name()办法。又由于是Gin的容器,用于办理Http的Handler的,需求注册路由和它对应的路由函数,所以需求完成Registry(r gin.IRoutes)办法,告诉gin,IoC容器中的实例,要注册哪些路由。

为什么要界说这样的标准呢?当然是为了不必每增加一个模块,就来IoC这儿面预界说一个变量呐。所以笼统了一套接口。大家就能够一致运用了。

var (
   // Gin依靠的 IoC 容器
   ginContainer = map[string]GinDependency{}
)

咱们一致将这些目标装入一个Map中,你或许会问,为什么要用Map呢?由于便利取出来,咱们每放入一个依靠时,给它标示一个唯一的姓名,之后需求取出来的时分,经过这个姓名就能够取出来了,这种一一对应的联系,用Map映射很便利。

那你或许又会问,Map 是并发不安全的,避免并发问题,不需求加锁?或许运用Sync.Map吗?哈哈,你说的没问题,考虑得还挺周到。可是咱们这些依靠目标,是需求在初始化的时分,就装载的。就像程序的装备文件一样,假如一开端发动的时分出现了问题,你还会发动程序吗?所以咱们这儿在发动后,最多只会读取IoC容器中的内容,并不会写入,所以不必担心并发问题。

2、供给几个办法

便利外界注入和运用依靠,供给几个函数,函数的逻辑也很简略,并且有对应的注释,能够简略看一下。我就不多赘述了。

// GinDI :将依靠注入此容器,Gin DI(Gin Dependency Inject)
func GinDI(dependency GinDependency) {
   dependencyName := dependency.Name()
   // 1、检查服务是否现已被注册
   if _, ok := ginContainer[dependencyName]; ok {
      panic(fmt.Sprintf("[ %s ]服务的依靠已在容器中,请勿重复注入", dependencyName))
   }
   // 2、未注入,放入容器
   ginContainer[dependencyName] = dependency
}
// GetGinDependency 依据模块称号 获取内部服务模块的依靠,外部运用时需自己断语,如:
//
// userGin = ioc.GetGinDependency("user").(user.UserGin)
func GetGinDependency(name string) GinDependency {
   if v, ok := ginContainer[name]; ok {
      return v
   } else {
      panic(fmt.Sprintf("容器中没有此依靠[ %s ]", name))
   }
}
// RegistryGin 注册一切的Gin Http 路由Handler
func RegistryGin(r gin.IRouter) {
  	// 初始化一切目标的路由
	for _, v := range ginApps {
        // 调用它的注册函数
		v.Registry(r)
	}
}
// ExistingGinDependencies 返回Gin HTTP服务依靠的容器中已存在的依靠称号
func ExistingGinDependencies() (apps []string) {
   for k, _ := range ginContainer {
      apps = append(apps, k)
   }
   return
}

可是你还没看到这儿面,并没有去调用对应的Init()办法,并没有看到我去初始化这个目标,所以还需求供给一个初始化的函数。又由于咱们开头说了,有多个容器,装着不一样的依靠目标,所以咱们能够供给一个一致初始化IoC的函数。

3、一致初始化函数

如下代码所示:咱们这儿供给了三个容器,有内部服务目标的容器、GRPC目标的容器、Gin的Handler目标的容器。咱们这儿面是用Gin的来举例的,其实每一个容器的完成迥然不同。能够看看详细代码~

// InitAllDependencies 用于初始IoC容器中的一切依靠
func InitAllDependencies() error {
   // 初始化内部服务模块依靠
   for _, v := range internalContainer {
      if err := v.Init(); err != nil {
         return err
      }
   }
   // 初始化GRPC服务依靠
   for _, v := range grpcContainer {
      if err := v.Init(); err != nil {
         return err
      }
   }
   // 初始化Gin HTTP服务依靠
   for _, v := range ginContainer {
      if err := v.Init(); err != nil {
         return err
      }
   }
   return nil
}

有了这个函数之后,咱们来看看怎样运用咱们的IoC容器吧

4、运用

  • 先看看是怎样将依靠目标放入容器的:
// 用于注入IOC中
var handler = &Handler{}
// Handler 经过一个实体类,把内部接口用HTTP露出出去【操控层Controller】
type Handler struct {
   service user.UserService
   log     *zap.SugaredLogger
}
// Registry 用于注册Handler所需求露出的路由
func (h *Handler) Registry(r route.IRoutes) {
   r.POST("/register/", h.Register)
   r.POST("/login/", h.Login)
   r.GET("/", h.GetUserInfo)
}
// Init 初始化Handler目标
func (h *Handler) Init() error {
   // 从内部服务的IOC中获取UserServiceImpl实例
   h.service = ioc.GetInternalDependency(user.AppName).(user.Service)
   // 怎样初始化日志目标
   h.log = zap.S().Named("USER HTTP")
   return nil
}
func (h *Handler) Name() string {
   return user.AppName // user
}
func init() {	
    // 将此Gin服务注入IOC中
    ioc.GinDI(handler)
}

User模块的Handler目标,完成了放入Gin容器的接口,将其放入Gin的IoC容器中GinDI()办法。并且告诉了,怎样初始化此目标Init()办法、注册什么路由Registry()办法、用什么姓名标识自己Name()办法

可是咱们这儿仅仅是注册好了。并没有人来初始化这个包哎,什么时分注入呢?

  • 什么时分注入

刚刚小cher说的,咱们能够经过导包的办法,来执行每个文件中的init()办法,也便是形如这样的

import (
	// 将目标放入内部服务的IoC中
        _ "github.com/Go-To-Byte/DouSheng/apps/user/impl" // UserService
        _ "github.com/Go-To-Byte/DouSheng/apps/video/impl" // VideoService
        // 将目标放入Gin服务的IoC中
	_ "github.com/Go-To-Byte/DouSheng/apps/user/http" // UserHandler 
	_ "github.com/Go-To-Byte/DouSheng/apps/video/http" // VideoHandler
        // ...
)

可是在哪里导入呢?当然,咱们能够选择在main办法中,编写上面的代码,这没问题。可是假如有许多这样的依靠需求被注入到IoC中,这儿这儿就有太多这样的导入代码了。

咱们能够将这些导包代码打包放入一个包中,之后在main办法中,就能够只导入那一个包了,详细代码下面一起贴。

导包没有问题了,也便是依靠都放入IoC中了,该何时初始化呢?

  • Main办法的代码

当然是在main办法中初始化呐,那我直接贴main办法的代码了~

package main
import (
    	// 在这个包里,一致将依靠放入IoC容器中
	_ "github.com/Go-To-Byte/DouSheng/apps/all"
    	// ...
)
func main() {
    // 1、加载装备文件
    if err := conf.LoadConfigFromToml(configFile); err != nil {
        panic(err)
    }
    // 2、初始化IoC的依靠
    if err := ioc.InitAllDependencies(); err != nil {
        return err
    }
    // 3、经过注册路由及对应的路由函数给Gin,并发动HTTP服务
    g := gin.Default()
    ioc.Registry(g)
    // 4、发动监听TCP衔接
    g.Run(conf.C().App.HttpAddr())
}

经过一套操作下来,每增加一个模块,Main这儿总算不必每次都去 New 目标、注册对应模块的路由了。

增加新模块时,只需求在那边将依靠放入到对应的IoC中,并且在去 all 里边导入所需求的包即可。

并且,有了IoC之后,编写单元测验的代码变得极为容易,比方我给你编写一个:

package impl_test
import (
   // 驱动加载一切需求放入IOC的实例
   _ "github.com/Go-To-Byte/DouSheng/user_center/common/all"
    // ...
)
var (
   service user.ServiceServer
)
func TestLogin(t *testing.T) {
   should := assert.New(t)
   u := user.NewLoginAndRegisterRequest()
   // 调用业务办法
   token, err := service.Login(context.Background(), u)
   if should.NoError(err) {
      t.Log(token)
   }
}
func init() {
   // 加载装备文件(或许能够从环境变量中读取)
   if err := conf.LoadConfigFromToml("../../../etc/config.toml"); err != nil {
      panic(err)
   }
   // 初始化IOC容器
   if err := ioc.InitAllDependencies(); err != nil {
      panic(err)
   }
   // 从IOC中获取接口完成
   service = ioc.GetGrpcDependency(user.AppName).(user.ServiceServer)
}

需求测验哪个模块,直接从IoC中获取对应的依靠,不必直接露出接口的完成。

至此,咱们简易版的IoC现已编写完成了,基本完成了需求。当然,假如你有个性化需求,你还能够持续拓展IoC,比方一致增加一些中间件(传入一些切片)…

详细的代码,能够看看咱们的项目中:项目IoC相关代码,是怎样运用的。

所以说,IoC仅仅一种怎样编写代码的指导思维,在许多当地都能够运用它来封装代码。假如文章对你有帮助,无妨点赞收藏运用起来~