golang gin大行其道!

共 5890字,需浏览 12分钟

 ·

2020-09-19 08:20

前言


很多人已经在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爱好者值得关注




浏览 94
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报