gin框架源码阅读

简介

源自:gin框架剖析(一)

gin 是目前 Go 里面使用最广泛的框架之一了,弄清楚 gin 框架的原理,有助于我们更好的使用 gin。这个系列 gin 源码阅读会逐步讲明白 gin 的原理,欢迎关注后续文章。

gin 简介

在读此文之前, 你可以带着以下几个问题去阅读:

request请求中的数据 是如何流转的?
gin框架到底扮演了什么角色?
request从gin流入net/http, 最后又是怎么回到gin中的?
gin的context为何能承担起来复杂的需求?
gin的路由算法?
gin的中间件是什么?
gin的Engine具体是个什么东西?
net/http的requeset, response都提供了哪些有用的东西?

开始

gin在pkg.go.dev中的官方文档docs

Installation

To install Gin package, you need to install Go and set your Go workspace first.

  1. You first need Go installed (version 1.14+ is required), then you can use the below Go command to install Gin.
$ go get -u github.com/gin-gonic/gin
  1. Import it in your code:
import "github.com/gin-gonic/gin"
  1. (Optional) Import net/http. This is required for example if using constants such as http.StatusOK.
import "net/http"

先从gin的官方第一个demo入手:

package main

import (
	"net/http"

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

func main() {
     //获取一个gin提供的默认的engine,可以通过gin.New来自己new一个空白无中间件的engine
    //gin.New() - New returns a new blank Engine instance without any middleware attached.
    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")
}

r.Run() 的源码:

// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely(无期限) unless an error happens.
//Run将路由器连接到http。服务器,并开始监听和服务HTTP请求。
//这是调用 http.ListenAndServe(addr, router) 的一个快捷方式
//注意:该方法将无限期地阻止调用goroutine,除非发生错误。
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
}

可以看出Run方法,实际调用的是http.ListenAndServe(addr, engine.Handler()), 这个函数是 net/http包提供的函数, 之后请求数据就会在net/http中开始流转。

Request 数据是如何流转的?

在使用gin之前,我们先看看net/http是怎么处理http请求的:

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("your request is received")) // 使用http.ResponseWriter提供的Write方法回消息
	})

    // 处理程序默认填个nil - 可以直接看源码
	if err := http.ListenAndServe(":8080", nil); err != nil {
		fmt.Println("start http server failed: ", err)
	}
}

在浏览器中输入localhost:8080, 会看到Hello World. 下面利用这个简单demo看下request的流转流程。
在这里插入图片描述

HTTP是如何建立起来的?

简单的说一下http请求是如何建立起来的:(需要有基本的网络基础, 可以找相关的书籍查看, 推荐看UNIX网络编程卷1:套接字联网API)
在这里插入图片描述
在TCP/IP五层模型下, HTTP位于应用层, 需要有传输层来承载HTTP协议. 传输层比较常见的协议是TCP,UDP, SCTP等. 由于UDP不可靠, SCTP有自己特殊的运用场景, 所以一般情况下HTTP是由TCP协议承载的(可以使用wireshark抓包然后查看各层协议)。

使用TCP协议的话, 就会涉及到TCP是如何建立起来的,在面试中关于TCP/IP协议能够常遇到的名词 1.三次握手, 2.四次挥手,3.挥手时的一些状态,4.以及在建立连接时,服务端如果只bind,但是并不listen,此时客户端朝这个socket发送数据会发生啥?,5.客户端先read,再write可以吗?等等之类的问题,就是在这里产生的. 具体的建立流程就不在陈述了, 大概流程如下图。

在这里插入图片描述
所以说, 要想能够对客户端http请求进行回应的话, 就首先需要建立起来TCP连接, 也就是socket.

下面要看下net/http是如何建立起来socket的

net/http 是如何建立 socket 的
简介

从图上可以看出, 不管server代码如何封装, 都离不开bind,listen,accept这些函数. 就从上面这个简单的demo入手查看源码。

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("your request is received")) // 使用http.ResponseWriter提供的Write方法回消息
	})

	if err := http.ListenAndServe(":8080", nil); err != nil {
		fmt.Println("start http server failed: ", err)
	}
}
注册路由

注册路由

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("your request is received")) // 使用http.ResponseWriter提供的Write方法回消息
})

这段代码是在注册一个路由及这个路由的handler到DefaultServeMux中。

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if handler == nil {
		panic("http: nil handler")
	}
	mux.Handle(pattern, HandlerFunc(handler))
}

// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()

	if pattern == "" {
		panic("http: invalid pattern")
	}
	if handler == nil {
		panic("http: nil handler")
	}
	if _, exist := mux.m[pattern]; exist {
		panic("http: multiple registrations for " + pattern)
	}

	if mux.m == nil {
		mux.m = make(map[string]muxEntry)
	}
	e := muxEntry{h: handler, pattern: pattern}
	mux.m[pattern] = e
	if pattern[len(pattern)-1] == '/' {
		mux.es = appendSorted(mux.es, e)
	}

	if pattern[0] != '/' {
		mux.hosts = true
	}
}

可以看到这个路由注册太过简单了, 也就给gin, iris, echo等框架留下了扩展的空间, 后面详细说这个东西。

服务监听及相应

服务监听及响应

上面路由已经注册到net/http了, 下面就该如何建立socket了, 以及最后又如何取到已经注册到的路由, 将正确的响应信息从handler中取出来返回给客户端。

1.创建 socket
if err := http.ListenAndServe(":8080", nil); err != nil {
	fmt.Println("start http server failed: ", err)
}
// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}
// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// If srv.Addr is blank, ":http" is used.
//
// ListenAndServe always returns a non-nil error. After Shutdown or Close,
// the returned error is ErrServerClosed.
func (srv *Server) ListenAndServe() error {
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	return srv.Serve(ln)
}
2.Accept 等待客户端链接
// Serve accepts incoming connections on the Listener l, creating a
// new service goroutine for each. The service goroutines read requests and
// then call srv.Handler to reply to them.
//
// HTTP/2 support is only enabled if the Listener returns *tls.Conn
// connections and they were configured with "h2" in the TLS
// Config.NextProtos.
//
// Serve always returns a non-nil error and closes l.
// After Shutdown or Close, the returned error is ErrServerClosed.
func (srv *Server) Serve(l net.Listener) error {
	// 省略部分代码
	ctx := context.WithValue(baseCtx, ServerContextKey, srv)
	for {
		rw, err := l.Accept()
		// 省略部分代码
		connCtx := ctx
		if cc := srv.ConnContext; cc != nil {
			connCtx = cc(connCtx, rw)
			if connCtx == nil {
				panic("ConnContext returned nil")
			}
		}
		// 省略部分代码
		go c.serve(connCtx)   // 看这里
	}
}
3.提供回调接口 ServeHTTP
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
	// 省略代码
	for {
		// 省略代码

		// HTTP cannot have multiple simultaneous active requests.[*]
		// Until the server replies to this request, it can't read another,
		// so we might as well run the handler in this goroutine.
		// [*] Not strictly true: HTTP pipelining. We could let them all process
		// in parallel even if their responses need to be serialized.
		// But we're not going to implement HTTP pipelining because it
		// was never deployed in the wild and the answer is HTTP/2.
		serverHandler{c.server}.ServeHTTP(w, w.req)
		w.cancelCtx()
		if c.hijacked() {
			return
		}
		w.finishRequest()
		// 省略代码
		c.rwc.SetReadDeadline(time.Time{})
	}
}
// net/http/serve.go L2858 - 2880
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.ServeHTTP(rw, req)
}
// net/http/serve.go L2414 - 2426
// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
	if r.RequestURI == "*" {
		if r.ProtoAtLeast(1, 1) {
			w.Header().Set("Connection", "close")
		}
		w.WriteHeader(StatusBadRequest)
		return
	}
	h, _ := mux.Handler(r)  // <-  看这里
	h.ServeHTTP(w, r)
}
4.回调到实际要执行的 ServeHTTP
// net/http/serve.go L2045-2048
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

以上基本是整个过程的代码。

ln, err := net.Listen(“tcp”, addr) 做了初试化, socket, bind, listen的操作。
rw, e := l.Accept()进行accept, 等待客户端进行连接。
go c.serve(connCtx) 启动新的goroutine来处理本次请求(因此http的请求处理函数,本身就被一个goroutine来承载了). 同时主goroutine继续等待客户端连接, 进行高并发操作。
h, _ := mux.Handler® 获取注册的路由, 然后拿到这个路由的handler, 然后将处理结果返回给客户端。

从这里也能够看出来, net/http基本上提供了全套(socket/bind/listen/accept/read/write/close)的服务。

为什么会出现很多go框架
// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
	// Check for exact match first.
	v, ok := mux.m[path]
	if ok {
		return v.h, v.pattern
	}

	// Check for longest valid match.  mux.es contains all patterns
	// that end in / sorted from longest to shortest.
	for _, e := range mux.es {
		if strings.HasPrefix(path, e.pattern) {
			return e.h, e.pattern
		}
	}
	return nil, ""
}

从这段函数可以看出来, 匹配规则过于简单, 当能匹配到路由的时候就返回其对应的handler, 当不能匹配到时就返回/. net/http的路由匹配根本就不符合 RESTful 的规则,遇到稍微复杂一点的需求时,这个简单的路由匹配规则简直就是噩梦(新版的http中match可能有所改变)。

所以基本所有的go框架干的最主要的一件事情就是重写net/http的route。我们直接说 gin就是一个 httprouter 也不过分, 当然gin也提供了其他比较主要的功能, 后面会一一介绍。

综述, net/http基本已经提供http服务的70%的功能, 那些号称贼快的go框架, 基本上都是提供一些功能, 让我们能够更好的处理客户端发来的请求. 如果你有兴趣的话,也可以基于 net/http 做一个 Go 框架出来。

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("your request is received")) // 使用http.ResponseWriter提供的Write方法回消息
	})

	if err := http.ListenAndServe(":8080", nil); err != nil {
		fmt.Println("start http server failed: ", err)
	}

这个例子中 http.HandleFunc 通过看源码,可以看到 URI “/” 被注册到了 DefaultServeMux 上。

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}
net/http ServeHTTP 的作用

net/http 里面有个非常重要的 Handler interface。只有实现了这个方法才能请求的处理逻辑引入自己的处理流程中。

// net/http/server.go L86 - 88
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

默认的 DefaultServeMux 就实现了这个 ServeHTTP。

这个 request 的流转过程:

socket.accept 接收到客户端请求后,启动 go c.serve(connCtx) [net/http server.go:L3034]行(或者直接在server.go中搜c.serve(connCtx)),专门处理这次请求,server 继续等待客户端连接。

获取能处理这次请求的 handler -> serverHandler{c.server}.ServeHTTP(w, w.req) [net/http server.go:L1930]。

跳转到真正的 ServeHTTP 去匹配路由,获取 handler。
由于并没有自定义路由,于是使用的是 net/http 默认路由。
所以最终调用去 DefaultServeMux 匹配路由,输出返回对应的结果。
在这里插入图片描述

探究 gin ServeHTTP 的调用链路

下面是 gin 的官方 demo, 仅仅几行代码,就启动了一个 echo server:

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

启动上面的http服务,浏览器输入

在这里插入图片描述
这段代码的大概流程:

r := gin.Default() 初始化了相关的参数。
将路由 /ping 以及对应的 handler 注册到路由树中。
使用 r.Run() 启动 server。
r.Run 的底层依然是 http.ListenAndServe。

// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
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
}

所以 gin 建立 socket 的过程,accept 客户端请求的过程与 net/http 没有差别,会同样重复上面的过程。唯一有差别的位置就是在于获取 ServeHTTP 的位置

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

   if req.URL != nil && strings.Contains(req.URL.RawQuery, ";") {
      var allowQuerySemicolonsInUse int32
      req = req.WithContext(context.WithValue(req.Context(), silenceSemWarnContextKey, func() {
         atomic.StoreInt32(&allowQuerySemicolonsInUse, 1)
      }))
      defer func() {
         if atomic.LoadInt32(&allowQuerySemicolonsInUse) == 0 {
            sh.srv.logf("http: URL query contains semicolon, which is no longer a supported separator; parts of the query may be stripped when parsed; see golang.org/issue/25192")
         }
      }()
   }

   handler.ServeHTTP(rw, req)
}

由于 sh.srv.Handler 是 interface 类型,但是其真正的类型是 gin.Engine,根据 interace 的动态转发特性,最终会跳转到 gin.Engine.ServeHTTP 函数中。

在这里插入图片描述
gin.ServeHTTP 的实现:

func (engine *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   c := engine.pool.Get().(*Context)
   c.writermem.reset(w)
   c.Requese = req
   c.reset()
   
   engine.handleHTTPRequeset(c)
   
   engine.pool.Put(c)
}

至此,终于我们看到了 gin.ServeHTTP 的全貌了

从 sync.pool 里面拿去一块内存,
对这块内存做初始化工作,防止数据污染,
处理请求 handleHTTPRequest,
请求处理完成后,把这块内存归还到 sync.pool 中,
现在看起来这个实现很简单,其实不然,这才是 gin 能够处理数据的第一步,也仅仅将请求流转入 gin 的处理流程而已。

这里做个结论:通过上面的源码流程分析,我们知道 net/http.ServeHTTP 这个函数相当重要性, 主要有这个函数的存在, 才能将请求流转入目前 Go 的这些框架里面。同学们有兴趣的话,可以去看看 echo, iris, go-zero 等框架是如何实现 ServeHTTP 的。

什么是路由?

这个其实挺容易理解的,就是根据不同的 URL 找到对应的处理函数即可。

目前业界 Server 端 API 接口的设计方式一般是遵循 RESTful 风格的规范。当然我也见过某些大公司为了降低开发人员的心智负担和学习成本,接口完全不区分 GET/POST/DELETE 请求,完全靠接口的命名来表示。

举个简单的例子,如:“删除用户”

在这里插入图片描述
这种 No RESTful 的方式,有的时候确实减少一些沟通问题和学习成本,但是只能内部使用了。这种不区分 GET/POST 的 Web 框架一般设计的会比较灵活,但是开发人员水平参差不齐,会导致出现很多“接口毒瘤”,等你发现的时候已经无可奈何了,如下面这些接口:
在这里插入图片描述这样的接口设计会导致开源的框架都是解析不了的,只能自己手动一层一层 decode 字符串,这里就不再详细铺开介绍了,等下一节说到 gin Bind 系列函数时再详细说一下。

继续回到上面 RESTful 风格的接口上面来,拿下面这些简单的请求来说:

在这里插入图片描述
这是比较规范的 RESTful API设计,分别代表:

获取 userID 的用户信息
更新 userID 的用户信息(当然还有其 json body,没有写出来)
创建 userID 的用户(当然还有其 json body,没有写出来)
删除 userID 的用户

可以看到同样的 URI,不同的请求 Method,最终其他代表的要处理的事情也完全不一样。

看到这里你可以思考一下,假如让你来设计这个路由,要满足上面的这些功能,你会如何设计呢?

gin 路由设计
简介

如何设计不同的 Method ?

通过上面的介绍,已经知道 RESTful 是要区分方法的,不同的方法代表意义也完全不一样,gin 是如何实现这个的呢?

其实很简单,不同的方法就是一棵路由树,所以当 gin 注册路由的时候,会根据不同的 Method 分别注册不同的路由树。
在这里插入图片描述
如这四个请求,分别会注册四颗路由树出来。

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
   // 省略代码
   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)

   // 省略代码
}

其实代码也很容易看懂:

拿到一个 method 方法时,去 trees slice 中遍历,
如果 trees slice 存在这个 method, 则这个URL对应的 handler 直接添加到找到的路由树上;
如果没有找到,则重新创建一颗新的方法树出来, 然后将 URL对应的 handler 添加到这个路由 树上。

路由的注册过程
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")
}

这段简单的代码里,r.GET 就注册了一个路由 /ping 进入 GET tree 中。这是最普通的,也是最常用的注册方式。

不过上面这种写法,一般都是用来测试的,正常情况下我们会将 handler 拿到 Controller 层里面去,注册路由放在专门的 route 管理里面,这里就不再详细拓展,等后面具体说下 gin 的架构分层设计。

//internal/route/controller/controller.go
func SomePostFunc(ctx *gin.Context) {
    context.String(http.StatusOK, "som post done")
}

//internal/route/route.go
xxx
func AddRoute() {
    router.POST("/somePost", controller.SomePostFunc)
}
使用 RouteGroup
func main() {
    router := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "pong",
        })
    })
    
    v1 := router.Group("v1")
    {
        v1.POST("login", func(context *gin.Context) {
            context.String(http.StatusOK, "v1 login")
        })
    }
    
    v2 := router.Group("v2")
    v2.Use(middleware.Log) //给整个路由组添加一个log中间件
    v2.POST("logout", controller.Logout) // 处理函数Logout放在一个目录名/包名都为为controller中
    
    router.Run()
}

RouteGroup 是非常重要的功能,举个例子:一个完整的 server 服务,url 需要分为鉴权接口和非鉴权接口,就可以使用 RouteGroup 来实现。其实最常用的,还是用来区分接口的版本升级。这些操作, 最终都会在反应到gin的路由树上

gin 路由的具体实现
func main() {
	router := gin.Default()
	router.GET("/ping", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "pong",
		})
	})
    
	router.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

还是从这个简单的例子入手。我们只需要弄清楚下面三个问题即可:

URL->ping 放在哪里了。
handler-> 放在哪里了。
URL 和 handler 是如何关联起来的。

1.GET/POST/DELETE/…的最终归宿
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}

在调用POST, GET, HEAD等路由HTTP相关函数时, 会调用handle函数。handle 是 gin 路由的统一入口。

// gin@xxx/routergroup.go L85-90
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()
}
2. 生成路由树

下面考虑一个情况,假设有下面这样的路由,你会怎么设计这棵路由树?

在这里插入图片描述
当然最简单最粗暴的就是每个字符串占用一个树的叶子节点,不过这种设计会带来的问题:占用内存会升高,我们看到 abc, abd, af 都是用共同的前缀的,如果能共用前缀的话,是可以省内存空间的。

gin 路由树是一棵前缀树. 我们前面说过 gin 的每种方法(POST, GET …)都有自己的一颗树,当然这个是根据你注册路由来的,并不是一上来把每种方式都注册一遍。

gin 每棵路由大概是下面的样子:

在这里插入图片描述
这个流程的代码太多,这里就不再贴出具体代码里,有兴趣的同学可以按照这个思路看下去即可。

  • handler 与 URL 关联
type node struct {
	path string
	indices string
	wildChild bool
	nType html.NodeType
	priority uint32
	children []*node
	handlers HandlersChain
	fullPath string
}

node 是路由树的整体结构:

children 就是一颗树的叶子结点。每个路由的去掉前缀后,都被分布在这些 children 数组里
path 就是当前叶子节点的最长的前缀
handlers 里面存放的就是当前叶子节点对应的路由的处理函数

当收到客户端请求时,如何找到对应的路由的handler?

第二篇说到 net/http 非常重要的函数 ServeHTTP,当 server 收到请求时,必然会走到这个函数里。由于 gin 实现这个 ServeHTTP,所以流量就转入 gin 的逻辑里面。

func (engine *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   c := engine.pool.Get().(*Context)
   c.writermem.reset(w)
   c.Requese = req
   c.reset()
   
   engine.handleHTTPRequeset(c)
   
   engine.pool.Put(c)
}

所以,当 gin 收到客户端的请求时, 第一件事就是去路由树里面去匹配对应的 URL,找到相关的路由, 拿到相关的处理函数。其实这个过程就是 handleHTTPRequest 要干的事情。

func (engine *Engine) handleHTTPRequest(c *Context) {
	// ...
	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.params != nil {
			c.Params = *value.params
		}
		if value.handlers != nil {
			c.handlers = value.handlers
			c.fullPath = value.fullPath
			c.Next()
			c.writermem.WriteHeaderNow()
			return
		}
		if httpMethod != "CONNECT" && rPath != "/" {
			if value.tsr && engine.RedirectTrailingSlash {
				redirectTrailingSlash(c)
				return
			}
			if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixePath) {
				return
			}
		}
		break
	}
}

从代码上看这个过程其实也很简单:

遍历所有的路由树,找到对应的方法的那棵树,
匹配对应的路由,
找到对应的 handler.

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值