golang gin大行其道!

2020年9月17日 226点热度 0人点赞 0条评论

前言


很多人已经在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)}
  1.  /banner/detail(路由最前面如果没有/开头的话,gin会自动补上/)

  2. /game/detail

  3. /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到connw.finishRequest()

总结

1. gin存储路由结构,用到了类似前缀树的数据结构,在存储方面可以共用前缀节省空间,但是查找方面应该不是很优秀,为什么不用普通的hash结构,key-value方式,存储路由和handlers,因为毕竟一个项目里面,定义的路由路径应该不会很多,使用hash存储应该不会占用很多内存。

但是如果每次请求都要去查找一下路由前缀树的话会比hash结构慢很多(请求量越大应该越明显)。

2. gin是基于net/http官方包来处理http请求整个流程

3. gin的中间件流程很好用


推荐阅读


学习交流 Go 语言,扫码回复「进群」即可

图片

站长 polarisxu

自己的原创文章

不限于 Go 技术

职场和创业经验

Go语言中文网

每天为你

分享 Go 知识

Go爱好者值得关注

图片


8810golang gin大行其道!

root

这个人很懒,什么都没留下

文章评论