Enjoy your life

AEnjoy’s Blog

Golang Opts VS IoC

在使用Golang设计一套系统的时候,我们通常会涉及到组件加载,依赖等相关问题,进而引申出这样一个问题:
Golang更适合使用IoC模式托管对象并进行依赖注入,还是适合使用opts方法加载并依赖对象?

本文将对这两个进行对比解答,同时附上对应的exampleCode

IoC

IoC (Inversion of control )控制反转/反转控制.
它是一种思想, 而不是一个技术实现.

如果你熟悉Java,你肯定知道IoC和Spring

它有这几个优点:

  • 解耦: 将对象的创建和管理从业务逻辑中分离,降低耦合度,提高代码可测试性。
  • 灵活: 可以方便地替换组件,实现热插拔。
  • 复用: 可以将对象配置信息集中管理,方便复用。

缺点:

  • 复杂性: 引入了一个额外的抽象层,增加了系统的复杂性。
  • 学习成本: 需要学习和掌握 IoC 框架的使用。

适合场景:

  • 大型复杂系统: 需要管理大量的对象和依赖关系。
  • 高度可配置的系统: 需要动态地调整系统配置。
  • 需要进行单元测试的系统: IoC 可以方便地模拟依赖对象。

它的实现Example:

最简单的,通过map来存储并托管对象,还可以根据Weight来指定加载顺序

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// service.go
package ioc

type Container interface {
Registry(name string, obj Object)
Get(name string) any
Init() error
}

type Object interface {
Init() error
Weight() uint16 // Add a Weight method to get the weight of the object
}

// map.go
package ioc

import (
"fmt"
"sort"
)

var _ Container = (*MapContainer)(nil)

type MapContainer struct {
name string
storge map[string]Object
}

func (c *MapContainer) Registry(name string, obj Object) {
c.storge[name] = obj
}

func (c *MapContainer) Get(name string) any {
return c.storge[name]
}

func (c *MapContainer) Init() error {
// Create a slice of object names and weights for sorting
type weightedObject struct {
name string
object Object
}

var weightedObjects []weightedObject

for name, obj := range c.storge {
weightedObjects = append(weightedObjects, weightedObject{name: name, object: obj})
}

// Sort by weight (ascending)
sort.Slice(weightedObjects, func(i, j int) bool {
return weightedObjects[i].object.Weight() < weightedObjects[j].object.Weight()
})

// Initialize objects in the order of their weights
for _, wObj := range weightedObjects {
if err := wObj.object.Init(); err != nil {
return fmt.Errorf("%s init error, %s", wObj.name, err)
}
fmt.Printf("[%s] %s init success with weight %d\n", c.name, wObj.name, wObj.object.Weight())
}

return nil
}

那我们该怎么使用呢?

当然也很简单:

准备托管对象的Container-Controller:

1
2
3
4
5
6
package ioc

var Controller Container = &MapContainer{
name: "controller",
storge: make(map[string]Object),
}

准备一个要托管给IoC的对象(通常是struct,实现了Object接口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package controller

type ControllerImpl struct {
db *gorm.DB
// other attribute
}

func (c *ControllerImpl) Init() error {
c.db = conf.C().DB.GetDB()
if c.db == nil {
panic("db is nil")
}
return nil
}
func (c *ControllerImpl) Weight() uint16{
return 0
}

func init() {
ioc.Controller.Registry("an-object", &ControllerImpl{}) // 注册对象
}

在初始化的时候导入这个模块即可

1
2
3
package main

import "example/controller"

使用:

1
ioc.Controller.Get("an-object").(controller.ControllerImpl)

Opts

这是Golang的一种语法糖,可以让我们动态添加参数.

优点:

  • 简单直观: 直接在函数调用时传递配置参数,易于理解。
  • 灵活: 可以根据需要选择传递哪些参数。

缺点:

  • 耦合度高: 配置参数与函数调用紧密耦合,不利于代码复用。
  • 可维护性差: 参数过多时,函数签名会变得很长,难以维护。

适合场景:

  • 小型简单系统: 对象数量较少,依赖关系不复杂。
  • 配置参数较少: 函数的参数列表比较短。
  • 不需要高度可配置的系统: 配置信息相对固定。

你会在Kubernetes,grpc等库中看到

如:

我们可以观察到,入口点使用了 … 表示不定长参数,而里面的函数返回是这个不定长参数的选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// grpc server.go
func NewServer(opt ...ServerOption) *Server {
opts := defaultServerOptions
for _, o := range globalServerOptions {
o.apply(&opts)
}
for _, o := range opt {
o.apply(&opts)
}
s := &Server{
lis: make(map[net.Listener]bool),
opts: opts,
conns: make(map[string]map[transport.ServerTransport]bool),
services: make(map[string]*serviceInfo),
quit: grpcsync.NewEvent(),
done: grpcsync.NewEvent(),
channelz: channelz.RegisterServer(""),
}
...
}

opts就被应用成功了

看不懂? 还有更简单的:

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
// main.go
import "example/core"
func main(){
app := core.NewApp(
core.SetHostName(hostName),
core.SetPort(listenPort),
)
app.Run()
}

// example/core/app.go
type app struct {
hostName string
port string
}

func NewApp(opts ...func(*app) error) *app {
var server = new(app)
for _, opt := range opts {
if err := opt(server); err != nil {
logger.Fatalf("Failed to apply option: %v", err)
}
}
return server
}

func SetHostName(hostName string) func(*app) error {
return func(s *app) error {
s.hostName = hostName
return nil
}
}

func SetPort(port string) func(*app) error {
return func(s *app) error {
s.port = port
return nil
}
}

通过opts,我们可以快速应用选项