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都提供了哪些有用的东西?
开始
:
Installation
To install Gin package, you need to install Go and set your Go workspace first.
- 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
- Import it in your code:
import "github.com/gin-gonic/gin"
- (Optional) Import
net/http
. This is required for example if using constants such ashttp.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.