简单使用
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello world")
})
r.POST("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
r.Run("0.0.0:8910")
}
通过几行代码,我们就可以创建一个http server,主要有以下三步
1.初始化引擎
2.注册路由
3.启动http server
初始化引擎
1. engine := New()主要是构造engine对象,因为我们用的默认配置,所以如下
有几个比较需要注意的字段
1).routerGroup,是管理路由的结构体,内嵌到engine中去了,我们注册路由和注册中间件都和它有关系,后面讲路由的时候再说
2).pool 然后是一个对象池的注册,context是从对象池里创建/复用的。
3)trees一个slice,存的就是path和handler的映射,gin为每一个方法(method,get,post),用一个基树来存储path和handlers
2.后面就是把2个中间件函数注册到router里
从代码里可以看到 RouterGroup实现了IRoutes的方法。另外中间件也是个handlerFunc函数,注册的话,也是追加到RouterGroup的handlers中。
注册路由
注册路由最终调用的是handle函数,即 engine.Get -> engine.routergroup.Get -> routergroup.handle
handle里主要做的事情就是
1.根据basePath和relativePath组成绝对路径
2.把routerGroup的handlers合并(已经注册过全局的中间件的和这次新注册的)
3.将当前method和absolutePath, handlers 绑定,比如根据 post /ping 找到handlers去执行。
这步调用的是addRoute方法
这里需要先介绍下路由和path映射的数据结构,基数树(Radix Tree)又称为PAT位树(Patricia Trie or crit bit tree),是一种更节省空间的前缀树(Trie Tree)。
还记得上面初始化过程中,trees那个字段吗?methodTree就是那个结构体,method就是方法,http中的get,post,delete等,root就是基树的根节点。所以说,gin为每一个http method创建一个基树来存储router。下面看一下node的结构
type node struct { path string // indices string wildChild bool nType nodeType priority uint32 children []*node // child nodes, at most 1 :param style node at the end of the array handlers HandlersChain // fullPath string }
对应如下图
也就是说当我们调用addRoute方法,就是往对应方法的基树中插入节点
Radix Tree
可以被认为是一棵简洁版的前缀树。我们注册路由的过程就是构造前缀树的过程,具有公共前缀的节点也共享一个公共父节点。假设我们现在注册有以下路由信息:
r := gin.Default()
r.GET("/", func1)
r.GET("/search/", func2)
r.GET("/support/", func3)
r.GET("/blog/", func4)
r.GET("/blog/:post/", func5)
r.GET("/about-us/", func6)
r.GET("/about-us/team/", func7)
r.GET("/contact/", func8)
那么我们会得到一个GET
方法对应的路由树,具体结构如下:
Priority Path Handle
9 \ *<1>
3 ├s nil
2 |├earch\ *<2>
1 |└upport\ *<3>
2 ├blog\ *<4>
1 | └:post nil
1 | └\ *<5>
2 ├about-us\ *<6>
1 | └team\ *<7>
1 └contact\ *<8>
上面最右边那一列每个*<数字>
表示Handle处理函数的内存地址(一个指针)。从根节点遍历到叶子节点我们就能得到完整的路由表。
为了获得更好的可伸缩性,每个树级别上的子节点都按Priority(优先级)
排序,其中优先级(最左列)就是在子节点(子节点、子子节点等等)中注册的句柄的数量。这样做有两个好处:
-
首先优先匹配被大多数路由路径包含的节点。这样可以让尽可能多的路由快速被定位。
-
类似于成本补偿。最长的路径可以被优先匹配,补偿体现在最长的路径需要花费更长的时间来定位,如果最长路径的节点能被优先匹配(即每次拿子节点都命中),那么路由匹配所花的时间不一定比短路径的路由长。下面展示了节点(每个
-
可以看做一个节点)匹配的路径:从左到右,从上到下
addRoute的逻辑大致如下:
先找到有无对应方法的root,这个地方trees用的slice []methodTree 而不是map map[method]root,所以每次查找都需要遍历[]methodTree,再根据里面的method字段来判断是否等于当前的method(这个地方用slice,答案是在短长度的情况下,slice的速度会比map快很多)
如果没有那么新建树,并且当前方法和 “/”就是根节点
1.空树直接插入当前节点
2.找到最长公共前缀
3.
// 分裂边缘(此处分裂的是当前树节点)
// 例如一开始path是search,新加入support,s是他们通用的最长前缀部分
// 那么会将s拿出来作为parent节点,增加earch和upport作为child节点
// 将新来的节点插入新的parent节点作为子节点
4.insertChild
函数是根据path
本身进行分割,将/
分开的部分分别作为节点保存,形成一棵树结构。参数匹配中的:
和*
的区别是,前者是匹配一个字段而后者是匹配后面所有的路径。
启动server
简单逻辑就是主协程listen,然后一个for 死循环里面,accept,然后没有一个可读写fd,开启一个goroutine去执行。后面就是一些http参数 协议的解析和数据组装。
http server最终这里处理请求
这个地方的context从之前提到过的pool里面获取,复用对象。做一些reset和初始化操作。然后处理请求, 用完之后在放到池子里去。
接下来走到handleHTTPRequest中去
这个地方就会涉及到路由匹配,然后执行对应的handlers。
先遍历trees,找到匹配的method。然后调用getValue找到匹配的path的节点。如果这个节点的handlers不为空,那么执行。执行hanlers,主要是Next方法,如下逻辑。
执行注册的handler回调函数,会传入context,这个执行顺序逻辑如下
假如handler如下,func1->func2->func3
func func1 () {
c.Next() //会调用func2()
}
func func2() {
c.Next() //会调用func3
}
func func3() {
doSomething()...
}
c.Next执行完后,会回到func2, func1继续执行后面的逻辑,所以一般可以在func1里面算下整个业务处理时间。
GitHub - gin-gonic/gin: Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin.
https://www.liwenzhou.com/posts/Go/read_gin_sourcecode/