目录

golang经典http库——gin

  gin作为golang的经典http库,被用在许多项目中,上周在某个情况下,被问及为什么在使用gin的时候panic后程序还能正常运行,那时候我的回答是肯定在某个地方统一recover了一下,接着我被追问在什么地方做的recover,我完全回答不出来。

  gin作为一个常用的http库,在多次使用后我仍然不清楚gin的设计,确实说不过去,在空余时间还是要了解一下gin的一些实现的。

简单的例子

func main() {
    g := gin.Default()
    g.Get("/",func(c *gin.Context){
        c..String(200,"hello")
    })
    g.Run(":8080")
}

1、gin路由原理

  在gin.Engine结构体中,有一个methodTrees类型的变量,这是methodTree类型的数组,这个数组存放各种请求方法,如POST、GET等。

// gin.go
type Engine struct {
    ...
    trees methodTrees
    ...
}
// tree.go
type methodTrees []methodTree

  当添加一个路由时,会调用group.handle方法,group.handle方法中主要做了一下几点处理:

  • 1、获取绝对路径;
  • 2、合并handler;
  • 3、添加路由信息。在第三步调用engine.addRoute函数中,会通过请求方法获取请求节点,如果该节点不存在则创建一个节点,最后调用root.addRoute添加路由。
// /gin/gin.go:329
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)

  engine.trees.get(method)方法返回的是一个路由节点结构体,其中path和fullPath分别代表当前节点的相对路径和绝对路径,handlers代表当前节点的执行函数,nType代表节点类型,children代表当前节点的子节点,因为gin支持:param参数,所以子节点数组中最多只有一个:param参数,在gin源码的备注中以及提及了。

// /gin/tree.go:116
type node struct {
	path      string        // 当前节点的相对路径
	indices   string
	wildChild bool
	nType     nodeType      // 节点类型
	priority  uint32
	children  []*node       // 子节点 // child nodes, at most 1 :param style node at the end of the arr
	handlers  HandlersChain // 执行函数
	fullPath  string        // 当前节点的绝对路径
}

  我们往gin中插入几个GET路由。

group := engine.Group("/")
group.GET("/auth", func(context *gin.Context) {...})
group.GET("/au", func(context *gin.Context) {...})
group.GET("/hello", func(context *gin.Context) {...})
group.GET("/hello/:name", func(context *gin.Context) {...})
group.GET("/hello/world", func(context *gin.Context) {...})
group.GET("/cc/world", func(context *gin.Context) {...})

  重点关注node结构体的addRoute(/gin/tree.go:152)方法,当第一次添加路由时,由于n.path和n.children长度都为0,所以直接调用n.insertChild添加一个节点并且将节点设置为root节点。

...
if len(n.path) == 0 && len(n.children) == 0 {
    n.insertChild(path, fullPath, handlers)
    n.nType = root
    return
}
...

  当节点中已有路由的时候,会先找到需要添加的节点的全路径与当前节点的全路径最大前缀,如果最大前缀小于当前节点的相对路径,则会更新当前节点。

...
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]
		}

		
		...

		// 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
	}
...

  然后判断公共前缀的长度是否小于需要插入的节点的绝对路径长度,如果小于则新建一个节点并把这个新的节点加入该节点的子节点中,其中还包含了一些路由规则的处理。

walk:
	for {
        ...
        // 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
    }
}

2、处理请求

  在gin.Run函数中主要执行了http.ListenAndServe函数,并把address和gin的httpHandler传入。然后使用h2c再包装一下,这里使用h2c估计是因为gin为了支持不使用tsl的http2。我们只需要关注gin对http.Handler的实现即可,也就是engin结构体的ServeHTTP方法。

// /gin/gin.go:376
func (engine *Engine) Handler() http.Handler {
	if !engine.UseH2C {
		return engine
	}

	h2s := &http2.Server{}
	return h2c.NewHandler(engine, h2s)
}

  gin在处理连接上使用了池管理连接上下文,每次客户端发起新的请求的时候就会从内存池中拿到一个上下文结构体,然后对其中的上下文结构体进行重置,这样做可以减少内存的分配与释放的开销。而在engine.handleHTTPRequest则是真正的对请求处理函数。

// /gin/gin.go:570
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)
}

  handleHTTPRequest函数中,首先获取了gin的路由树,然后遍历路由树找出相应的请求方法并获取该方法节点,然后调用root.getValue获取一个nodeValue,并判断其中是否有handler,如果有则执行如果没有则继续其他逻辑。而其中获取路由的过程可以看作添加路由的反向操作。

func (engine *Engine) handleHTTPRequest(c *Context) {
    httpMethod := c.Request.Met
	...
	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, c.skippedNodes, unescape)
		if value.params != nil {
			c.Params = *value.params
		}
		if value.handlers != nil {
			c.handlers = value.handlers
			c.fullPath = value.fullPath
			c.Next()
			c.writermem.WriteHeaderNow()
			return
		}
		...
	}
    ...
	c.handlers = engine.allNoRoute
	serveError(c, http.StatusNotFound, default404Body)
}

3、关于panic后的处理

异常是如何被捕获的?

  在偶然情况下,被问到了如果在handler中panic了,gin会怎么处理,其实gin并没有处理异常,这个异常是在go的http库中处理的,那时候还不了解,但根据我的推测应该是在gin的某一个地方统一recover了一下。
  通过查看gin.Default代码发现,创建engine的时候调用了engine.Use函数,其中就传入了一个Recovery函数,Recovery函数又调用了CustomRecoveryWithWriter函数。

// /gin/gin.go:216
func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}
// /gin/recovery.go:33
func Recovery() HandlerFunc {
	return RecoveryWithWriter(DefaultErrorWriter)
}
// /gin/recovery.go:43
func RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc {
	if len(recovery) > 0 {
		return CustomRecoveryWithWriter(out, recovery[0])
	}
	return CustomRecoveryWithWriter(out, defaultHandleRecovery)
}

  而在CustomRecoveryWithWriter函数函数中,返回了一个func闭包,其中defer执行了recover捕获了错误。

// /gin/recovery.go:51
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
	...
	return func(c *Context) {
		defer func() {
			if err := recover(); err != nil {
				...
			}
		}()
		c.Next()
	}
}

假设gin没有recovery

  • engine.Use(Logger(), Recovery())改成engine.Use(Logger())后,panic后程序会崩溃吗?   答案是不会。在gin.Run函数中最后其实是调用了http.ListenAndServe函数,而gin相当于提供了一个httpHandler。
// /gin/gin.go:376
func (engine *Engine) Run(addr ...string) (err error) {
	defer func() { debugPrintError(err) }()

	if engine.isUnsafeTrustedProxies() {
		debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
			"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
	}

	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	err = http.ListenAndServe(address, engine.Handler())
	return
}

  http.ListenAndServe监听完tcp后,调用srv.Serve对监听tcp连接并对客户端连接进行处理,然后获取连接的context并使用协程调用c.serve正式开始对http请求的处理。在c.serve函数中,就对错误进行了recover。

func (c *conn) serve(ctx context.Context) {
    ...
    defer func() {
		if err := recover(); err != nil && err != ErrAbortHandler {
			const size = 64 << 10
			buf := make([]byte, size)
			buf = buf[:runtime.Stack(buf, false)]
			c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
		}
		...
	}()
    ...
}

  所以即使gin不对handler的错误进行捕获程序也不会崩溃。

洋葱模型demo


import (
	"fmt"
	"time"
)

type Context struct {
	*Handler
	index int
}

func NewContext() *Context {
	return &Context{
		Handler: &Handler{
			handlers: []func(ctx *Context){},
		},
		index: -1,
	}
}

type Handler struct {
	handlers []func(c *Context)
}

func (c *Context) AddFunc(fn func(ctx *Context)) {
	c.handlers = append(c.handlers, fn)
}

func (c *Context) Next() {
	c.index++
	fmt.Println("index:", c.index)
	for c.index < len(c.handlers) {
		c.handlers[c.index](c)
		c.index++ // 防止多次执行同一个handler
	}
}

func main() {
	fnRecovery := func(c *Context) {
		fmt.Println("USE RECOVERY")
		defer func() {
			if err := recover(); err != nil {
				fmt.Println("recovery")
			}
		}()
		c.Next()
	}
	fnPanic := func(c *Context) {
		fmt.Println("USE PANIC")
		panic("over")
	}
	c := NewContext()
	c.AddFunc(fnRecovery)
	c.AddFunc(fnPanic)
	c.Next()
	time.Sleep(time.Second * 10)
}