Gin的一个小demo,蕴藏着大道变化

2023年 8月 9日 42.2k 0

孤独是一个陪伴人一生的伙伴,是一个既定事实,与其否认,与其抗争,与其无谓的逃避,不如接受它,拥挤的人群里让它保护你回家,周六的上午让它陪你吃早餐,整理阳光。

目标

package main

import (
  "net/http"

  "github.com/gin-gonic/gin"
)

func main() {
  r := gin.Default()
  r.GET("/ping", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
      "message": "pong",
    })
  })
  r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

这是官方文档中 Running Gin 的栗子,我们也从这里开始看

gin.Default()

可以看到一开始就会调用 Default 方法,所以我们也从此处入手,看到源码中的 Default 函数

func Default() *Engine {

    debugPrintWARNINGDefault()

    engine := New()

    engine.Use(Logger(), Recovery())

    return engine

}

可以看到 Default 做了以下事情:

  • 返回值一个 Engine 实例
  • 调用 debugPrintWARNINGDefault 方法
  • 创建 Engine 实例
  • engine 调用 Use 方法,参数为两个函数的返回值(Logger,Recovery)
  • Engine 是什么?

    Engine 是一个结构体,其中定义了很多属性。

    type Engine struct {
    
        RouterGroup
    
        RedirectTrailingSlash bool
    
        RedirectFixedPath bool
    
        HandleMethodNotAllowed bool
    
        ForwardedByClientIP bool
    
        AppEngine bool
    
        UseRawPath bool
    
        UnescapePathValues bool
    
        RemoveExtraSlash bool
    
        RemoteIPHeaders []string
       
        TrustedPlatform string
    
        MaxMultipartMemory int64
    
        UseH2C bool
    
        ContextWithFallback bool
    
    
    
        delims           render.Delims
    
        secureJSONPrefix string
    
        HTMLRender       render.HTMLRender
    
        FuncMap          template.FuncMap
    
        allNoRoute       HandlersChain
    
        allNoMethod      HandlersChain
    
        noRoute          HandlersChain
    
        noMethod         HandlersChain
    
        pool             sync.Pool
    
        trees            methodTrees
    
        maxParams        uint16
    
        maxSections      uint16
    
        trustedProxies   []string
    
        trustedCIDRs     []*net.IPNet
    
    }
    

    什么?这也太多了,眼花缭乱,暂时先不用管,用到哪个再看哪个

    既然这样,New 方法无非就是返回了这个了,赋了下初始值咯。

    创建 Engine 实例

    func New() *Engine {
    
        debugPrintWARNINGNew()
    
        engine := &Engine{
    
            RouterGroup: RouterGroup{
    
                Handlers: nil,
    
                basePath: "/",
    
                root:     true,
    
            },
    
            FuncMap:                template.FuncMap{},
    
            RedirectTrailingSlash:  true,
    
            RedirectFixedPath:      false,
    
            HandleMethodNotAllowed: false,
    
            ForwardedByClientIP:    true,
    
            RemoteIPHeaders:        []string{"X-Forwarded-For", "X-Real-IP"},
    
            TrustedPlatform:        defaultPlatform,
    
            UseRawPath:             false,
    
            RemoveExtraSlash:       false,
    
            UnescapePathValues:     true,
    
            MaxMultipartMemory:     defaultMultipartMemory,
    
            trees:                  make(methodTrees, 0, 9),
    
            delims:                 render.Delims{Left: "{{", Right: "}}"},
    
            secureJSONPrefix:       "while(1);",
    
            trustedProxies:         []string{"0.0.0.0/0", "::/0"},
    
            trustedCIDRs:           defaultTrustedCIDRs,
    
        }
    
        engine.RouterGroup.engine = engine
    
        engine.pool.New = func() any {
    
            return engine.allocateContext(engine.maxParams)
    
        }
    
        return engine
    
    }
    

    Use() 做了什么?

    找到 Engine 的方法 Use,先看它里面写的什么吧

    func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
    
        engine.RouterGroup.Use(middleware...)
    
        engine.rebuild404Handlers()
    
        engine.rebuild405Handlers()
    
        return engine
    
    }
    

    从字面意思就可以看出它的作用,就是添加中间件,这个函数的核心应该就是 engine.RouterGroup.Use(middleware...) 这串代码了吧,那在看看它究竟做了什么吧。

    进入 RouterGroup.Use()

    首先,从上面 struct Engine{} 中就可以看出 RouterGroup 也是一个结构体

    type RouterGroup struct {
    
        Handlers HandlersChain
    
        basePath string
    
        engine   *Engine
    
        root     bool
    
    }
    
    func (group *RouterGroup) returnObj() IRoutes {
    
        if group.root {
    
            return group.engine
    
        }
    
        return group
    
    }
    
    
    func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    
        group.Handlers = append(group.Handlers, middleware...)
    
        return group.returnObj()
    
    }
    

    看到这个 Use 方法,好像明白了它作用了,原来就是把 middleware 添加进 group.Handlers 中,那么再继续查看 HandlersChain 是什么

    type HandlerFunc func(*Context)
    
    type HandlersChain []HandlerFunc
    

    它就是一个元素为 HandlerFuncslice,所以反向结论,中间件也是一个 HandlerFunc

    到现在我们回顾一下,我们目前做完操作,最后得到的结果是什么

    engine := &Engine{
    	RouterGroup: RouterGroup{
    
    	            Handlers: [Logger(), Recovery()],
    
    	            basePath: "/",
    
    	            root:     true,
    
    	},
    	...
    }
    

    最后 engine 相当于 Handlers添加了两个函数,至此 Default 函数完。

    r.GET()

    GET 方法在里面又做了什么呢,通过 Goland ,按住 ctrl + 点击,跳转到源码可以看到GET 方法主要是为了调用一个 handle 的方法

    func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    
        return group.handle(http.MethodGet, relativePath, handlers)
    
    }
    

    所以搞清楚 handle 做了什么,就知道 GET 做了什么

    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()
    
    }
    

    从字面意思可以理解为:

  • group.calculateAbsolutePath(relativePath) 可能是将相对路径转化成绝对路径
  • group.combineHandlers(handlers) 看样子可能是处理存入 handlers 的中间件
  • group.engine.addRoute(httpMethod, absolutePath, handlers) 将请求方法,处理后的路径,和要执行的 handlers 不知道是怎么处理一下,搞成了路由
  • 猜想完毕,带着猜想进入函数内部一探究竟!

    calculateAbsolutePath()

    func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
        return joinPaths(group.basePath, relativePath)
    }
    

    可以看到,这个函数内部也只是调用了函数 joinPaths ,继续深入:

    func joinPaths(absolutePath, relativePath string) string {
    
        if relativePath == "" {
    
            return absolutePath
    
        }
        finalPath := path.Join(absolutePath, relativePath)
    
        if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
    
            return finalPath + "/"
    
        }
    
        return finalPath
    }
    
    func lastChar(str string) uint8 {
        if str == "" {
            panic("The length of the string can't be 0")
        }
        return str[len(str)-1]
    }
    
    

    这样很明显可以看出主要目的就是将 group.basePathrelativePath 合并,relativePath 我们知道是自己在方法中传递的值,那 group.basePath 呢?
    回到 New 方法中查看了下,在初始化生成的时候,basePath: "/"absolutePath 的值为 /ping

    combineHandlers()

    func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
    	finalSize := len(group.Handlers) + len(handlers)
            
    	assert1(finalSize < int(abortIndex), "too many handlers")
            
    	mergedHandlers := make(HandlersChain, finalSize)
            
    	copy(mergedHandlers, group.Handlers)
            
    	copy(mergedHandlers[len(group.Handlers):], handlers)
            
    	return mergedHandlers
    }
    
    

    这个函数主要功能就是将 中间件 和对应的处理函数,放在同一个 slice 中,可能为了后面的执行顺序,这里 handlers 的值就变成了 [Logger(), Recovery(), handler()]

    addRoute()

    addRoute 应该可以说是这里最重要的逻辑处理了吧,看看它内部做了什么吧

    func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
    	...other code
    
    	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)
    
    	...other code
    }
    
    

    首先,看着这段代码,来猜测一下,它的意思以及作用:

  • root := engine.trees.get(method),可能是为了从 trees 中获取对应 method 的根节点
  • engine.trees = append(engine.trees, methodTree{method: method, root: root}) 不存在根节点,那就创建根节点
  • root.addRoute(path, handlers) 存在根节点那就直接在根节点中添加
  • engine.trees.get()

    进入到 get 方法中查看

    type methodTrees []methodTree
    
    func (trees methodTrees) get(method string) *node {
    
    	for _, tree := range trees {
            
    		if tree.method == method {
                    
    			return tree.root
                            
    		}
    	}
    	return nil
    }
    
    

    它会遍历 engine.trees,找出为 GET 方法创建好的节点并返回

    创建节点

    当还没有创建这个方法的根节点时,就会新建 methodTree

    methodTree{method: method, root: root}
    

    root.addRoute()

    这个函数执行就是整体路由的核心了,所以必须要知道它在做什么(这个函数实现有点复杂,分两部分看)

    // 第一部分
    func (n *node) addRoute(path string, handlers HandlersChain) {
    	fullPath := path
    	n.priority++
    
    	// Empty tree
    	if len(n.path) == 0 && len(n.children) == 0 {
            
    		n.insertChild(path, fullPath, handlers)
    		n.nType = root
    		return
                    
    	}
    
    	parentFullPathIndex := 0
    
        // 第二部分
        ...other code
    }
    
    

    我们上面已知 pathhandlers 的值,从这第一部分可以看出,也就是创建根节点的时候的操作。接下来继续看第二部分

    // 第二部分
    walk:
    	for {
    		// Find the longest common prefix.
    		// This also implies that the common prefix contains no ':' or '*'
    		// since the existing key can't contain those chars.
    		i := longestCommonPrefix(path, n.path)
    
    		// Split edge
    		if i < len(n.path) {
    			child := node{
    				path:      n.path[i:],
    				wildChild: n.wildChild,
    				nType:     static,
    				indices:   n.indices,
    				children:  n.children,
    				handlers:  n.handlers,
    				priority:  n.priority - 1,
    				fullPath:  n.fullPath,
    			}
    
    			n.children = []*node{&child}
    			// []byte for proper unicode char conversion, see #65
    			n.indices = bytesconv.BytesToString([]byte{n.path[i]})
    			n.path = path[:i]
    			n.handlers = nil
    			n.wildChild = false
    			n.fullPath = fullPath[:parentFullPathIndex+i]
    		}
    
    		// Make new node a child of this node
    		if i < len(path) {
    			path = path[i:]
    			c := path[0]
    
    			// '/' after param
    			if n.nType == param && c == '/' && len(n.children) == 1 {
    				parentFullPathIndex += len(n.path)
    				n = n.children[0]
    				n.priority++
    				continue walk
    			}
    
    			// Check if a child with the next path byte exists
    			for i, max := 0, len(n.indices); i = len(n.path) && n.path == path[:len(n.path)] &&
    					// Adding a child to a catchAll is not possible
    					n.nType != catchAll &&
    					// Check for longer wildcard, e.g. :name and :names
    					(len(n.path) >= len(path) || path[len(n.path)] == '/') {
    					continue walk
    				}
    
    				// Wildcard conflict
    				pathSeg := path
    				if n.nType != catchAll {
    					pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
    				}
    				prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
    				panic("'" + pathSeg +
    					"' in new path '" + fullPath +
    					"' conflicts with existing wildcard '" + n.path +
    					"' in existing prefix '" + prefix +
    					"'")
    			}
    
    			n.insertChild(path, fullPath, handlers)
    			return
    		}
    
    		// Otherwise add handle to current node
    		if n.handlers != nil {
    			panic("handlers are already registered for path '" + fullPath + "'")
    		}
    		n.handlers = handlers
    		n.fullPath = fullPath
    		return
    	}
    
    

    代码太长就不一一列举了,这个里面总共做了:

  • 查找最长公共前缀,就是为了找到当前路径与已经存在的路由节点路劲的最长共同部分,便于是否需要将节点进行分割
  • 分割边,就是如果最长的公共前缀的长度小于节点路径 n.path 的长度,说明当前路径与已经存在的部分节点路由有重合部分,但还有剩余部分不同,需要进行分割,并创建新节点表示剩余部分
  • 处理参数和通配符,就是最长公共前缀大于的长度与n.path相同,且当前路径还有剩余部分,则根据当前路径的第一个字符判断如何处理参数(例如::id, *filepath),如果当前路径是参数或通配符类型,则需要将新节点插入到已存在的参数或通配符节点的子节点列表中。人话:["/", "a", ":id"],与 ["/", "a", "123"],长度相同,但是不一样
  • 插入节点,如果当前路径为普通路径(即不是参数,也不是通配符),就会将新节点插入到当前节点的子节点列表中
  • 注册处理函数,根据路径找到对应节点,将处理函数,添加进节点的 handles 中
  • 通过 walk 不断循环更新路由树的节点的路径,处理函数等属性,保持路由树的结构完整和正确,这样就实现了路由功能
  • 自此,r.get() 便算完了

    r.Run()

    最后项目需要 Run 起来,调用Run函数,那 Run 函数是怎么样 run 起来的呢?

    func (engine *Engine) Run(addr ...string) (err error) {
        ...other code
    
    	address := resolveAddress(addr)
    	debugPrint("Listening and serving HTTP on %s\n", address)
    	err = http.ListenAndServe(address, engine.Handler())
    	return
    }
    
    

    这里只有两句代码核心,那就是 resolveAddress启动http服务

    resolveAddress()

    func resolveAddress(addr []string) string {
    	switch len(addr) {
    	case 0:
    		if port := os.Getenv("PORT"); port != "" {
    			debugPrint("Environment variable PORT=\"%s\"", port)
    			return ":" + port
    		}
    		debugPrint("Environment variable PORT is undefined. Using port :8080 by default")
    		return ":8080"
    	case 1:
    		return addr[0]
    	default:
    		panic("too many parameters")
    	}
    }
    
    

    这里不管其它,只看为 0,我们没有传参数,默认为 :8080 端口

    http.ListenAndServe()

    这里第一个参数,想必都知道了就是端口,第二个参数需要传递一个含有 ServeHTTP 方法的结构体就行,它会被当作一个 HTTP处理器

    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)
    }
    
    

    这里面主要就是为后续操作绑定上下文,以及将路由到匹配的处理函数执行,也就是你在写 handler 函数时,使用到的 context

    r.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "pong",
        })
    })
    
    

    这样一个简单的例子,所走过的流程,就全部完成了(其中还有些函数没有拿出来看,知道作用就好)

    总结

  • 带有 ServeHTTP 方法的结构体,就可以被当作成一个 HTTP处理器,也就是每个请求都会通过 ServeHTTP
  • 对路由的处理是生成了一个路由前缀树,通过给对应树中节点,添加处理函数,达到路由和函数对应的关系,当请求进来的时候,会查询路由树,然后找到对应节点,执行对应节点上的处理函数
  • 分组和设置直接设置路由,抽象概念上,它们是同一种逻辑
  • 中间件和 handler 函数也是同一种逻辑,只有执行顺序的不同
  • 相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论