框架"零件"

context

  1. 框架级 Context:可以组合优秀框架(如 gin.Context)的基础上,扩展自己常用的功能函数。
  2. 业务级 Context:针对具体的业务,组合框架 Context,封装更多的业务工具函数,进一步提升效率。
  3. 如果有必要进步提升性能的话,可以使用 sync.Pool 对 Context 进行管理,避免 Context 频繁创建销毁带来的性能损耗。
  4. 灵活使用链路调用,有助于提升代码的清晰度和可扩展性。

路由匹配

Gin 使用 radix tree,尽可能压缩路由的公共前缀,同时使用 indices 加速路由的检索。

radix tree

中间件

使用洋葱型中间件,可以很方便地进行 AOP 编程,有很大的扩展性。

如 Gin 框架中:

1
2
3
4
5
// HandlersChain defines a HandlerFunc slice.
type HandlersChain []HandlerFunc

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

同时为 IRouter 接口也定义 Group 函数,这样可以进一步提升聚合类的逻辑复用:

1
2
3
4
5
// IRouter defines all router handle interface includes single and group router.
type IRouter interface {
IRoutes
Group(string, ...HandlerFunc) *RouterGroup
}

在执行过程中,使用 c.Next()c.Abort() 来进行处理器调用或提前退出等逻辑控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const abortIndex int8 = math.MaxInt8 >> 1

func (c *Context) Next() {
c.index++ // 初始化 c.index = -1
for c.index < int8(len(c.handlers)) {
if c.handlers[c.index] != nil {
c.handlers[c.index](c)
}
c.index++
}
}

func (c *Context) Abort() {
c.index = abortIndex // 设置为最大值,后面的 next 就会直接返回
}

扩展现有框架

相比于自己从零开始写一个 Web 框架,完全可以站在前人的肩膀上,将流行的、好用的、开源协议允许的框架代码拷贝到自己的仓库中,进行改造升级,从而快速搭建一个功能完善、经过验证、贴合团队需要的强悍 Web 框架。

一切皆服务

按照面向接口编程的理念,将每个模块看成是一个服务,服务的具体实现我们其实并不关心,我们关心的是服务提供的能力,即接口协议。那么框架主体真正要做的事情是什么呢?其实是:定义好每个模块服务的接口协议,规范服务与服务之间的调用,并且管理每个服务的具体实现

所有的服务都去框架主体中注册自身的模块接口协议,其他的服务调用功能模块的时候,并不是直接去这个服务获取实例,而是从框架主体中获取有这个接口协议的服务实例。

一切皆服务

容器 container

服务提供接口定义可参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// NewInstance is a function that creates a new instance of a service
type NewInstance func(...any) (any, error)

// ServiceProvider is an interface that defines a service provider
type ServiceProvider interface {
// Register a service provider into the container,
// whether to initialize the service or not determined by the IsDefer method
Register(Container) NewInstance
// Boot the service provider, this method will be called after the container is initialized.
// Is id recommend to do some initialization work in this method.
// If returns error, the service initialization will be failed.
Boot(Container) error
// IsDefer determines whether the service provider should be deferred.
// If true, the service provider will be deferred until the first time the service is used.
IsDefer() bool
// Params are the parameters which would be passed to the NewInstance function.
Params(Container) []any
// Name is a method that returns the unique name of the service provider.
Name() string
}

容器接口定义可参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Container is a service provider container, provides methods to register and resolve service providers.
type Container interface {
// Bind binds a service provider into the container,
// if the service provider is already bound, it would panic.
Bind(provider ServiceProvider) error
// IsBind checks if a service provider is bound into the container
IsBind(key string) bool
// Make resolves a service provider from the container
Make(key string) (any, error)
// MustMake resolves a service provider from the container, if not found, it will panic
MustMake(key string) any
// MakeNew creates a new instance of a service provider,
// it is useful when you need to create a new instance of a service provider
// and pass some different parameters to the service provider's constructor.
MakeNew(key string, params ...any) (any, error)
}

接口实现可参考:

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// Helps to check if the Container interface is implemented
var _ Container = (*HdwebContainer)(nil)

// HdwebContainer is the default implementation of the Container interface
type HdwebContainer struct {
// providers is a map of service providers, key is the name of the service provider
providers map[string]ServiceProvider
// instances is a map of service instances, key is the name of the service
instances map[string]any
// lock is used to protect the container from concurrent access
lock sync.RWMutex
}

// NewHdwebContainer creates a new HdwebContainer
func NewHdwebContainer() *HdwebContainer {
return &HdwebContainer{
providers: make(map[string]ServiceProvider),
instances: make(map[string]any),
lock: sync.RWMutex{},
}
}

// Bind binds a service provider into the container,
// if the service provider is already bound, '
// it will replace the existing one and return an error.
func (h *HdwebContainer) Bind(provider ServiceProvider) error {
h.lock.Lock()
key := provider.Name()
if _, ok := h.providers[key]; ok {
h.lock.Unlock()
panic("service provider already bound: " + key)
}
h.providers[key] = provider
h.lock.Unlock()

if provider.IsDefer() {
return nil
}

if err := provider.Boot(h); err != nil {
return err
}

params := provider.Params(h)
method := provider.Register(h)

instance, err := method(params...)
if err != nil {
return err
}

h.lock.Lock()
defer h.lock.Unlock()
if _, ok := h.instances[key]; ok {
panic("service provider already resolved: " + key)
}
h.instances[key] = instance
return nil
}

// IsBind checks if a service provider is bound into the container
func (h *HdwebContainer) IsBind(key string) bool {
return h.findServiceProvider(key) != nil
}

// Make resolves a service provider from the container
func (h *HdwebContainer) Make(key string) (any, error) {
return h.make(key, nil, false)
}

// MakeNew creates a new instance of a service provider,
// it is useful when you need to create a new instance of a service provider
// and pass some different parameters to the service provider's constructor.
func (h *HdwebContainer) MakeNew(key string, params ...any) (any, error) {
return h.make(key, params, true)
}

// MustMake resolves a service provider from the container, if not found, it will panic
func (h *HdwebContainer) MustMake(key string) any {
ins, err := h.make(key, nil, false)
if err != nil {
panic(err)
}
return ins
}

func (h *HdwebContainer) make(key string, params []any, forceNew bool) (any, error) {
sp := h.findServiceProvider(key)
if sp == nil {
return nil, errors.New("service provider not found: " + key)
}
if forceNew {
return h.newInstance(sp, params)
}

if ins := h.getInstance(key); ins != nil {
return ins, nil
}

h.lock.Lock()
defer h.lock.Unlock()
if ins, ok := h.instances[key]; ok {
return ins, nil
}
ins, err := h.newInstance(sp, params)
if err != nil {
return nil, err
}
h.instances[key] = ins
return ins, nil
}

func (h *HdwebContainer) getInstance(key string) any {
h.lock.RLock()
defer h.lock.RUnlock()
return h.instances[key]
}

func (h *HdwebContainer) newInstance(sp ServiceProvider, params []any) (any, error) {
if err := sp.Boot(h); err != nil {
return nil, err
}
if params == nil {
params = sp.Params(h)
}
method := sp.Register(h)
return method(params...)
}

func (h *HdwebContainer) findServiceProvider(key string) ServiceProvider {
h.lock.RLock()
defer h.lock.RUnlock()
return h.providers[key]
}

服务 service provider

  • contract:服务功能接口定义
  • provider:为服务实现 ServiceProvider 接口
  • service:实现服务 contract 功能接口

框架级服务

1
2
3
4
5
6
7
8
9
10
11
12
- contract  # 服务接口定义
- app.go
- kernel.go
- env.go
- config.go
- provider # 服务实现
- app # app 服务
- provider.go # 实现 Service Provider
- service.go # 实现服务接口
- kernel
- provider.go
- service.go

业务级服务

1
2
3
4
5
6
7
8
9
10
11
- {root}  # 根目录
- app # app 目录
- provider # 通用业务服务
- user # user 服务
- contract.go # user 服务接口定义
- service.go # user 服务接口实现
- provider.go # 为 user 服务实现 Service Provider
- mail
- contract.go
- service.go
- provider.go

自动化 DRY

在业务开发过程中,对于那些重复性的类模板劳动,可以使用 CLI 命令行工具,或其他自动化工具,来简化这些劳动输出。

这里有 2 个思路,一个是使用 Makefile,一个是使用 CLI(Go 里面可以使用 cobra 框架)。选择的时候可以考虑以下几个点:

  1. 命令变动的频率(二者在这一点区别不大,不过如果变动频率比较低,那 CLI 的劣势就相对可以忽略了)
  2. 命令使用的复杂性(参数越多,则需要越详尽的帮助说明)
  3. 业务逻辑相关性(越相关,则逻辑越复杂,使用代码越好管控)

常见的思路有:

  • 生成项目脚手架(init)
  • 项目启动管理(build、start、stop、restart、update)
  • 服务模版生成(provider list/new)—— 可以结合 survey 做命令行渐进式输入,template 生成模板代码
  • 命令行系列生成(command list/new)
  • 定时任务(cron list/run)
  • swagger 生成(swagger gen)