golang gin大行其道!
前言
很多人已经在api接口项目这块,已经用上了gin,我自己也用上了,感觉挺好用的,就写了这篇文章来分享下我对gin的理解和拾遗。gin对于我的理解还不能算是一个api框架,因为它是基于net/http包来处理http请求处理的,包括监听连接,解析请求,处理响应都是net/http包完成的,gin主要是做了路由前缀树的构建,包装http.request,http.response,还有一些快捷输出响应内容格式的工具功能(json.Render,xml.Render,text.Render,proto.Render)等,接下来主要从两个方面去分析gin。
gin构建路由
gin处理请求
一,gin构建路由
下面是gin用来构建路由用到的结构代码
type Engine struct {
RouterGroup
trees methodTrees
省略代码
}
// 每个http.method (GET,POST,HEAD,PUT等9种)都会构建成单独的前缀树
type methodTree struct {
// 方法: GET,POST等
method string
// 对应方法的前缀树的根节点
root *node
}
//GET,POST前缀树等构成的切片
type methodTrees []methodTree
type node struct {
// 节点的当前路径
path string
// 子节点的首字母构成的字符串,数量和位置对应children切片
indices string
// 子节点
children []*node
// 当前匹配到的路由,处理的handler
handlers HandlersChain
// 统计子节点优先级
priority uint32
// 节点类型
nType nodeType
maxParams uint8
wildChild bool
// 当前节点的全路径,从root到当前节点的全路径
fullPath string
}
用户定义路由
router := gin.New()
router.GET("banner/detail", PkgHandler)
router.PrintTrees()
router.GET("/game/detail", PkgHandler)
router.PrintTrees()
router.GET("/geme/detail", PkgHandler)
router.GET 调用链
这里提一下HandlerFunc,就是gin常说的中间件,是一个函数类型,只要实现该函数类型就可以用作中间件来处理逻辑
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
assert1(path[0] == '/', "path must begin with '/'")
assert1(method != "", "HTTP method can not be empty")
assert1(len(handlers) > 0, "there must be at least one handler")
debugPrintRoute(method, path, handlers)
// 根据method取出当前方法前缀树的root
root := engine.trees.get(method)
if root == nil {
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
// 这里就是构建路由前缀树的逻辑(避免阅读不友好,这里就不继续贴下去了)
root.addRoute(path, handlers)
}
/
banner/detail(路由最前面如果没有/开头的话,gin会自动补上/)
/game/detail
/geme/detail
上面三个路由地址,构建路由前缀树的过程(这里不讨论:和 *)
最后的路由树结构就是上面这个样子
gin的RouterGroup路由组的概念(group和use方法),主要是给后面添加路由用来传递path和handlers和继承之前path和handlers,这里不细讲
二,gin处理请求
先简述下net/http包处理的整个流程
1. 解析数据包,解析成http请求协议,存在http.request, 包装请求头,请求体,当前连接conn【read(conn) 读取数据】
2. 外部实现handler接口,调用外部逻辑处理请求(这里外部就是gin)
3. 根据用户逻辑,返回http响应给客户端 存在http.response,包装响应头,响应体,当前连接conn【write(conn) 返回数据】
这是net/http包提供给外部,用于处理http请求的handler接口,只要外部实现了此接口,
并且传递给http.server.Handler,就可以执行用户handler逻辑,gin的Engine实现了Handler 接口,
所以gin就介入了整个http请求流程中的用户逻辑那部分。即上面的第二点。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
serverHandler{c.server}.ServeHTTP(w, w.req)
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
//在这里最终会调用用户传进来的handler接口的实现,并且把rw = http.response,req = http.request
传递到gin处理逻辑里面去,这样gin既能读取http请求内容,也能操作gin输出到客户端
handler.ServeHTTP(rw, req)
}
// ServeHTTP conforms to the http.Handler interface.
// gin.Engine实现了http.Handler接口,可以处理请求
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
//请求完成,放回池中
engine.pool.Put(c)
}
所以,gin处理请求的入口ServeHTTP这里
1. 每个请求都会用到单独gin.context,包装http.response, http.request,通过在中间件中传递gin.context,从而用户可以拿到请求相关信息,根据自己逻辑可以处理请求和响应,context也用了pool池化,减少内存分配
2. 根据上面传递的请求路径,和method,在对应方法的路由前缀树上查询到节点,即就找到要执行的handlers,就执行用户定义的中间件逻辑(前缀树的查询也跟构建流程类似,可以参考上面的图)
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
unescape := false
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues
}
if engine.RemoveExtraSlash {
rPath = cleanPath(rPath)
}
fmt.Printf("httpMethod: %s, rPath: %s\n", httpMethod, rPath)
// Find root of the tree for the given HTTP method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// Find route in tree
// 根据请求路径获取对应方法树上的节点
value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
// 获取对应节点上的中间件(即handler)
c.handlers = value.handlers
fmt.Printf("c.handlers: %d\n", len(c.handlers))
c.Params = value.params
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
省略代码
}
省略代码
}
handlers有2种方式
1. 在handler中不手动调用c.next的话,类似于队列,先进先执行
2. 如果手动调用c.next的话,类似于洋葱模型,包裹剥开概念的执行
func (c *Context) Next() {
//c.index从-1开始
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
中断整个调用链,从当前函数返回
func (c *Context) Abort() {
//直接将中间件索引改成最大限制的值,从而退出for循环
c.index = abortIndex
}
贴下图,方便理解这个中间件是怎么依次执行的
gin执行完handlers之后,就回归到net/http包里面,就会finishRequest()
serverHandler{c.server}.ServeHTTP(w, w.req)
w.cancelCtx()
if c.hijacked() {
return
}
flush数据, write到conn
w.finishRequest()
总结
1. gin存储路由结构,用到了类似前缀树的数据结构,在存储方面可以共用前缀节省空间,但是查找方面应该不是很优秀,为什么不用普通的hash结构,key-value方式,存储路由和handlers,因为毕竟一个项目里面,定义的路由路径应该不会很多,使用hash存储应该不会占用很多内存。
但是如果每次请求都要去查找一下路由前缀树的话会比hash结构慢很多(请求量越大应该越明显)。
2. gin是基于net/http官方包来处理http请求整个流程
3. gin的中间件流程很好用
推荐阅读
站长 polarisxu
自己的原创文章
不限于 Go 技术
职场和创业经验
Go语言中文网
每天为你
分享 Go 知识
Go爱好者值得关注