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 {RouterGrouptrees methodTrees省略代码}// 每个http.method (GET,POST,HEAD,PUT等9种)都会构建成单独的前缀树type methodTree struct {// 方法: GET,POST等method string// 对应方法的前缀树的根节点root *node}//GET,POST前缀树等构成的切片type methodTrees []methodTreetype node struct {// 节点的当前路径path string// 子节点的首字母构成的字符串,数量和位置对应children切片indices string// 子节点children []*node// 当前匹配到的路由,处理的handlerhandlers HandlersChain// 统计子节点优先级priority uint32// 节点类型nType nodeTypemaxParams uint8wildChild 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取出当前方法前缀树的rootroot := 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.Handlerif 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 = reqc.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.MethodrPath := c.Request.URL.Pathunescape := falseif engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {rPath = c.Request.URL.RawPathunescape = 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 methodt := engine.treesfor 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.handlersfmt.Printf("c.handlers: %d\n", len(c.handlers))c.Params = value.paramsc.fullPath = value.fullPathc.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的中间件流程很好用
推荐阅读
站长 polarisxu
自己的原创文章
不限于 Go 技术
职场和创业经验
Go语言中文网
每天为你
分享 Go 知识
Go爱好者值得关注
