当前Go语言生态中的WEB开发框架已经有很多了,比如 gin、beego、iris 等,各有各的特点,经过这几年的沉淀已经足够优秀了。那我还不拿来主义直接用,还要再造个轮子干啥呢?大致是因为这几点考虑:

  1. gin足够简洁高效,但太简了,我需要继续封装点东西,方便后期开发
  2. beego特性丰富,不过封装的层级太多,代码量大看着头晕,封装过多性能下降
  3. iris没有深入了解,感觉和beego有点像,也还是复杂了。
  4. 写点项目更好的研究Go语言

Gin的一些问题

Gin的使用过程中我发现下面一些问题。

第一:参数路由和普通路由不能并存

gin 的路由模块使用了 httprouter 的实现,并稍作改动,看下面:

1
2
3
4
5
6
7
# 模式A
/user_info/:id
/user_info/:id/name

# 模式B
/name/a/b
/name/:id

模式A是支持的,但是模式B这两条路由规则居然不能同时存在?为什么要做这个限制呢?

第二:Handlers的处理方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
	finalSize := len(group.Handlers) + len(handlers)
	if finalSize >= int(abortIndex) {
		panic("too many handlers")
	}
	mergedHandlers := make(HandlersChain, finalSize)
	copy(mergedHandlers, group.Handlers)
	copy(mergedHandlers[len(group.Handlers):], handlers)
	return mergedHandlers
}

上面的代码看出来,gin将所有grouphandlers和当前路由自己的handlers合并成了一个slice,这种handlers的设计,让业务逻辑实现不够清晰和灵活,每个路由函数对应的树节点中存放一个slice,这里面按顺序存放了所有处理函数的函数指针,路由和处理函数多的时候这种结构会大量占用内存。gin底层限制处理函数最多有62个。

第三:中间件不可随意写

1
2
3
4
5
6
7
8
9
app.GET("/chende/a", handler("chende-a"))
app.Use(func (ctx *gin.Context) {
	log.Println("The before middle chende")
	ctx.Next()
	log.Println("The after middle chende")
})
app.GET("/chende/b", handler("chende-b"))
app.Use(gin.Logger(), gin.Recovery())
app.GET("/chende/c", handler("chende-c"))

大家猜猜上面添加的中间件处理函数对那些路由产生影响?

测试之后就会发现,Use添加的中间件处理函数只对后面添加的路由起作用,这不大符合常理。一般一个分组的中间件应该对这个分组下的所有路由起作用。

第四:插件嵌套不够自然

 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
func Logger() gin.HandlerFunc {
	return func(c *gin.Context) {
		t := time.Now()
		// 给Context实例设置一个值
		c.Set("name", "chende")
		// 请求前
		c.Next()
		// 请求后
		latency := time.Since(t)
		log.Print(latency)
	}
}
func Doing() gin.HandlerFunc {
	return func(c *gin.Context) {
		// TODO: do something
		c.Next()
	}
}
func main() {
	gin.SetMode(gin.ReleaseMode)
	app := gin.Default()
	
    // 下面这两行一定不能颠倒顺序
	app.Use(Logger())
    app.Use(Doing())
	
	_ = app.Run(":8090")
}

上面Logger是为了打印当前请求执行时间,处理前记录一个时间,处理后记录一个时间,计算时间差就知道请求执行时间了。问题就出在这里,如果后面还有别的插件,加入的顺序必须要放在这个插件后面,否则统计时间就不准了。

如果插件比较多的时候,特别是使用第三方插件的时候,稍不注意可能会产生非预期的结果。这种层层嵌套加载插件的方式在Node.js中大量存在,是一种异步特性的框架。这不符合Go本身多协程,代码同步执行的特点。

GoFast的改变

gin是一个非常棒的开源Web Framework,使用的人很多。好的方面自不必说,在使用过程中发现一些地方换一种实现方式,可能更友好;或者会想到能不能进一步提高gin的性能呢?于是GoFast诞生了。

GoFast的目标:以gin为基础展开改造,逐步替代,突出灵活性和高性能,最后形成自有特色的WEB Framework

针对上面已经提出的三个问题,GoFast做了改变:

第一:支持参数后面再加特征路由

比如下面这种,可以解析到不同的路由处理函数。

1
2
3
4
5
6
7
// 同时支持
/user_info/:id
/user_info/:id/name

// 下面这样的路由是不能同时支持的
/ 或者 /root
/:name/id

第二:引入上下文生命周期的概念

我们知道客户请求到达net.http的时候,底层获取协议信息,并稍作处理之后就调用goroutines进入handler处理函数,每个请求对应一个协程。当请求信息到达handler的时候我们可以看做本次请求进入了自己的全生命周期,为此我们定义一些比如Before、Valid、After、Send等一些常规的事件,并按顺序内置于框架中,使用者可以随意定义一些钩子函数,加入对应的生命周期进行处理。

这种方式能让开发思路更清晰。

第三:数组实现底层核心数据机构

框架封装的好坏,性能和内存占用是一项重要参考指标。大量的堆内存使用,碎片化过多,占用内存过大等都会影响程序的性能。为此我专门为路由树和路由处理函数设计了占用内存更小的,而且用数组实现的底层数据结构,希望能带来性能的改善。

其它问题思考

我还发现一些问题应该是可以改进的:

A. Gin的RedixTree的子节点采取最深最优先比对的算法

依据是你子节点最多,可能将来包含的请求数最多,优先试图匹配你这个分支。可现实情况中不同分支的负载压力是极度不对称的,也就意味着可能最浅的层请求数量最多,应该优先比对。

GoFast由于采取了预热路由的设计,那么比较容易解决这个问题。只要统计不同分支的访问评率(实际项目中也需要这个特性),就可以区分优先级了。接着变换一次路由树搞定。

B. 标准net.http包中接收到请求之后做了大量解析工作

调试源代码后我发现net.http包中的代码可能并不高效,可能为了通用性,在每次得到http请求包的时候,做了大量的比对工作,量实在是有点大,肯定影响到性能了,这个值得研究。

或者直接看看号称更快的http库:fasthttp

(未完待续…)