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

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

目标

Go 复制代码
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 函数

Go 复制代码
func Default() *Engine {

    debugPrintWARNINGDefault()

    engine := New()

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

    return engine

}

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

  1. 返回值一个 Engine 实例
  2. 调用 debugPrintWARNINGDefault 方法
  3. 创建 Engine 实例
  4. engine 调用 Use 方法,参数为两个函数的返回值(Logger,Recovery)

Engine 是什么?

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

Go 复制代码
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 实例

Go 复制代码
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,先看它里面写的什么吧

Go 复制代码
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 也是一个结构体

Go 复制代码
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 是什么

Go 复制代码
type HandlerFunc func(*Context)

type HandlersChain []HandlerFunc

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

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

Go 复制代码
engine := &Engine{
	RouterGroup: RouterGroup{

	            Handlers: [Logger(), Recovery()],

	            basePath: "/",

	            root:     true,

	},
	...
}

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

r.GET()

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

Go 复制代码
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {

    return group.handle(http.MethodGet, relativePath, handlers)

}

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

Go 复制代码
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()

}

从字面意思可以理解为:

  1. group.calculateAbsolutePath(relativePath) 可能是将相对路径转化成绝对路径
  2. group.combineHandlers(handlers) 看样子可能是处理存入 handlers 的中间件
  3. group.engine.addRoute(httpMethod, absolutePath, handlers) 将请求方法,处理后的路径,和要执行的 handlers 不知道是怎么处理一下,搞成了路由

猜想完毕,带着猜想进入函数内部一探究竟!

calculateAbsolutePath()

Go 复制代码
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
    return joinPaths(group.basePath, relativePath)
}

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

Go 复制代码
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()

Go 复制代码
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 应该可以说是这里最重要的逻辑处理了吧,看看它内部做了什么吧

Go 复制代码
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
}

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

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

engine.trees.get()

进入到 get 方法中查看

Go 复制代码
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

Go 复制代码
methodTree{method: method, root: root}

root.addRoute()

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

Go 复制代码
// 第一部分
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 的值,从这第一部分可以看出,也就是创建根节点的时候的操作。接下来继续看第二部分

Go 复制代码
// 第二部分
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 < max; i++ {
				if c == n.indices[i] {
					parentFullPathIndex += len(n.path)
					i = n.incrementChildPrio(i)
					n = n.children[i]
					continue walk
				}
			}

			// Otherwise insert it
			if c != ':' && c != '*' && n.nType != catchAll {
				// []byte for proper unicode char conversion, see #65
				n.indices += bytesconv.BytesToString([]byte{c})
				child := &node{
					fullPath: fullPath,
				}
				n.addChild(child)
				n.incrementChildPrio(len(n.indices) - 1)
				n = child
			} else if n.wildChild {
				// inserting a wildcard node, need to check if it conflicts with the existing wildcard
				n = n.children[len(n.children)-1]
				n.priority++

				// Check if the wildcard matches
				if len(path) >= 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
	}

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

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

自此,r.get() 便算完了

r.Run()

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

Go 复制代码
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()

Go 复制代码
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处理器

Go 复制代码
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

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

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

总结

  1. 带有 ServeHTTP 方法的结构体,就可以被当作成一个 HTTP处理器,也就是每个请求都会通过 ServeHTTP
  2. 对路由的处理是生成了一个路由前缀树,通过给对应树中节点,添加处理函数,达到路由和函数对应的关系,当请求进来的时候,会查询路由树,然后找到对应节点,执行对应节点上的处理函数
  3. 分组和设置直接设置路由,抽象概念上,它们是同一种逻辑
  4. 中间件和 handler 函数也是同一种逻辑,只有执行顺序的不同
相关推荐
桃园码工6 小时前
4-Gin HTML 模板渲染 --[Gin 框架入门精讲与实战案例]
前端·html·gin·模板渲染
桃园码工7 小时前
1-Gin介绍与环境搭建 --[Gin 框架入门精讲与实战案例]
go·gin·环境搭建
云中谷7 小时前
Golang 神器!go-decorator 一行注释搞定装饰器,v0.22版本发布
go·敏捷开发
苏三有春15 小时前
五分钟学会如何在GitHub上自动化部署个人博客(hugo框架 + stack主题)
git·go·github
Narutolxy16 小时前
深入探讨 Go 中的高级表单验证与翻译:Gin 与 Validator 的实践之道20241223
开发语言·golang·gin
Hello.Reader16 小时前
全面解析 Golang Gin 框架
开发语言·golang·gin
我是前端小学生1 天前
Go语言中的方法和函数
go
探索云原生1 天前
在 K8S 中创建 Pod 是如何使用到 GPU 的: nvidia device plugin 源码分析
ai·云原生·kubernetes·go·gpu
桃园码工2 天前
2-Gin 框架中的路由 --[Gin 框架入门精讲与实战案例]
gin·路由·实战案例
海绵波波1072 天前
Gin-vue-admin(2):项目初始化
vue.js·golang·gin