本文首發於 深刻淺出 Gin 生命週期 轉載請註明出處。
Gin 是一個用 Go (Golang) 編寫的 web 框架,因爲出色的性能優點而被普遍使用,這裏咱們就來分析下 Gin 的請求生命週期php
先來了解下其目錄結構:html
. ├── binding 依據 HTTP 請求 Accept 解析響應數據格式 │ ├── binding.go │ ├── binding_nomsgpack.go │ ├── default_validator.go │ ├── form.go │ ├── form_mapping.go │ ├── header.go │ ├── json.go │ ├── msgpack.go │ ├── multipart_form_mapping.go │ ├── protobuf.go │ ├── query.go │ ├── uri.go │ ├── xml.go │ ├── yaml.go ├── ginS │ └── gins.go ├── internal │ ├── bytesconv │ │ ├── bytesconv.go │ └── json │ ├── json.go │ └── jsoniter.go ├── render 依據解析的 HTTP 請求 Accept 響應格式生成響應 │ ├── data.go │ ├── html.go │ ├── json.go │ ├── msgpack.go │ ├── protobuf.go │ ├── reader.go │ ├── redirect.go │ ├── render.go │ ├── text.go │ ├── xml.go │ └── yaml.go ├── auth.go ├── *context.go ├── context_appengine.go ├── debug.go ├── deprecated.go ├── errors.go ├── fs.go ├── *gin.go ├── logger.go ├── mode.go 設置 Gin 運行環境模式 ├── path.go Path 處理 ├── recovery.go 處理 Panic 的 Recovery 中間件 ├── *response_writer.go ResponseWriter ├── *routergroup.go 路由組設置 ├── tree.go 路由算法 ├── utils.go helper 函數 └── version.go
其中比較重要的模塊爲: context.go,gin.go,routergroup.go,以及 tree.go;分別處理 HTTP 請求及響應上下文,gin 引擎初始化,路由註冊及路由查找算法實現。git
binding 目錄內提供基於 HTTP 請求消息頭 Context-Type 的 MIME 信息自動解析功能,相對應的 Render 目錄下提供具體數據格式渲染的實現方法。github
本文着重介紹 Gin 實現一個 Web 服務器,從請求到達到生成響應整個生命週期內的核心功能點,這將有助於咱們理解 Gin 的執行原理和之後的開發工做的展開。golang
先從官網的第一個 demo example.go 出發:web
package main import "github.com/gin-gonic/gin" func main() { // 建立 Gin Engine 實例 r := gin.Default() // 設置請求 URI /ping 的路由及響應處理函數 r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) // 啓動 Web 服務,監聽端口,等待 HTTP 請求到並生成響應 r.Run() // 監聽並在 0.0.0.0:8080 上啓動服務 }
經過執行 go run example.go 命令來運行代碼,它會啓動一個阻塞進程監聽並等待 HTTP 請求:算法
# 運行 example.go 而且在瀏覽器中訪問 0.0.0.0:8080/ping $ go run example.go
從代碼中咱們能夠看出經過 Gin 實現一個最簡單的 Web 服務器,只需 3 個步驟:編程
1)建立 Gin 實例
2)註冊路由及處理函數
3)啓動 Web 服務json
Gin 實例建立經過 gin.Default() 方法完成,其定義在 gin.go#L159 文件裏:數組
// Default returns an Engine instance with the Logger and Recovery middleware already attached. func Default() *Engine { debugPrintWARNINGDefault() engine := New() engine.Use(Logger(), Recovery()) return engine }
Default() 方法實現以下功能:
1)建立 Gin 框架對象 Engine
2)配置 Gin 默認的中間件,Logger() 和 Recovery(),其實現分別位於 logger.go 和 recovery.go 文件內
3)返回 Gin 框架對象
其中 New() 方法會實例化 Gin 的 Engine 對象,gin.go#L129
// New returns a new blank Engine instance without any middleware attached. // By default the configuration is: // - RedirectTrailingSlash: true // - RedirectFixedPath: false // - HandleMethodNotAllowed: false // - ForwardedByClientIP: true // - UseRawPath: false // - UnescapePathValues: true func New() *Engine { debugPrintWARNINGNew() engine := &Engine{ RouterGroup: RouterGroup{ Handlers: nil, basePath: "/", root: true, }, FuncMap: template.FuncMap{}, RedirectTrailingSlash: true, RedirectFixedPath: false, HandleMethodNotAllowed: false, ForwardedByClientIP: true, AppEngine: defaultAppEngine, UseRawPath: false, RemoveExtraSlash: false, UnescapePathValues: true, MaxMultipartMemory: defaultMultipartMemory, trees: make(methodTrees, 0, 9), delims: render.Delims{Left: "{{", Right: "}}"}, secureJSONPrefix: "while(1);", } engine.RouterGroup.engine = engine engine.pool.New = func() interface{} { return engine.allocateContext() } return engine }
實例化比較核心的功能是:
1)初始化 Engine 對象, 關鍵步驟是初始化路由組 RouterGroup。
2)初始化 pool, 這是核心步驟. pool 用來存儲 context 上下文對象. 用來優化處理 http 請求時的性能。
後面會重點分析 engine.pool 的實現細節。
Engine 是 Gin 框架的核心引擎 gin.go#L56,數據結構以下:
type Engine struct { RouterGroup // 關鍵:路由組 // 設置開關 RedirectTrailingSlash bool RedirectFixedPath bool HandleMethodNotAllowed bool ForwardedByClientIP bool AppEngine bool UseRawPath bool UnescapePathValues bool MaxMultipartMemory int64 RemoveExtraSlash bool // 界定符 delims render.Delims secureJSONPrefix string HTMLRender render.HTMLRender FuncMap template.FuncMap allNoRoute HandlersChain allNoMethod HandlersChain noRoute HandlersChain noMethod HandlersChain pool sync.Pool // 關鍵:context 處理 trees methodTrees maxParams uint16 }
Engine 結構體內部除一些功能性開關設置外,核心的就是 RouterRroup,pool 和 trees。Gin 的全部組件都是由 Engine 驅動。
完成 Gin 的實例化以後,咱們能夠經過 r.GET("/ping", func(c *gin.Context) {}) 定義 HTTP 路由及處理 handler 函數。
以 gin.GET 爲例,展開源碼以下:
// 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) }
gin.GET 定義 HTTP GET 請求的路由及處理方法,並返回 IRoutes 對象實例。
// RouterGroup is used internally to configure router, a RouterGroup is associated with // a prefix and an array of handlers (middleware). type RouterGroup struct { Handlers HandlersChain basePath string engine *Engine root bool }
RouterGroup routergroup.go#L41 用於配置路由,其中:
在 gin.GET 方法內部經過調用 group.handle() routergroup.go#L72 方法添加路由:
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() }
路由就和 Engine 綁定好關係了。
// IRoutes defines all router handle interface. type IRoutes interface { Use(...HandlerFunc) IRoutes Handle(string, string, ...HandlerFunc) IRoutes Any(string, ...HandlerFunc) IRoutes GET(string, ...HandlerFunc) IRoutes POST(string, ...HandlerFunc) IRoutes DELETE(string, ...HandlerFunc) IRoutes PATCH(string, ...HandlerFunc) IRoutes PUT(string, ...HandlerFunc) IRoutes OPTIONS(string, ...HandlerFunc) IRoutes HEAD(string, ...HandlerFunc) IRoutes StaticFile(string, string) IRoutes Static(string, string) IRoutes StaticFS(string, http.FileSystem) IRoutes }
IRoute 是個接口類型,定義了 router 所需的 handle 接口,RouterGroup 實現了這個接口。
推而廣之,Gin 還支持以下等路由註冊方法:
它們都定義在 routergroup.go 文件內。
Gin 實例化和路由設置後工做完成後,咱們進入 Gin 生命週期執行的核心功能分析,Gin 到底是如何啓動 Web 服務,監聽 HTTP 請求並執行 HTTP 請求處理函數生成響應的。這些工做通通從 gin.Run() 出發 gin.go#L305:
// 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) }() address := resolveAddress(addr) debugPrint("Listening and serving HTTP on %s\n", address) err = http.ListenAndServe(address, engine) return }
gin.Run() 是 net/http 標準庫 http.ListenAndServe(addr, router) 的簡寫,功能是將路由鏈接到 http.Server 啓動並監聽 HTTP 請求。
由此,咱們不得不放下手頭的工做,率先了解下 net/http 標準庫的執行邏輯。
net/http 標準庫的 ListenAndServe(addr string, handler Handler) 方法定義在 net/http/server.go#L3162 文件裏。
func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() }
在 ListenAndServe(addr string, handler Handler) 內部則調用的是 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)// 啓動服務等待鏈接 }
而後,執行 srv.Serve(ln) 即 Server.Serve(l net.Listener) server.go#L2951,在 net.Listen("tcp", addr) 等待鏈接,建立新的 goroutine 來處理請求和生成響應的業務邏輯:
func (srv *Server) Serve(l net.Listener) error { ... for { rw, e := l.Accept() if e != nil { select { case <-srv.getDoneChan(): return ErrServerClosed default: } ... return e } if cc := srv.ConnContext; cc != nil { ctx = cc(ctx, rw) if ctx == nil { panic("ConnContext returned nil") } } tempDelay = 0 c := srv.newConn(rw) c.setState(c.rwc, StateNew) // before Serve can return go c.serve(ctx) // 啓動 Web 服務 } }
最後,進入到 go c.serve(ctx) 啓動 Web 服務,讀取 HTTP 請求數據,生成響應 server.go#L1817:
// Serve a new connection. func (c *conn) serve(ctx context.Context) { ... // HTTP/1.x from here on. for { w, err := c.readRequest(ctx)// 讀取 HTTP 去請求 ... // 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) ... } }
最終,調用 r.Run() 方法傳入的 Engine 來執行 serverHandler{c.server}.ServeHTTP(w, w.req) 處理接收到的 HTTP 請求和生成響應,這裏將響應處理的控制權交回給 Gin Engine。
Go 標準庫 net/http 提供了豐富的 Web 編程接口支持,感興趣的朋友能夠深刻研究下 net/http 標準庫源碼,瞭解其實現細節。
Engine.ServeHTTP 是 Gin 框架核心中的核心,咱們來看下它是如何處理請求和響應的:
// ServeHTTP conforms to the http.Handler interface. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := engine.pool.Get().(*Context) // 從臨時對象池 pool 獲取 context 上下文對象 c.writermem.reset(w) c.Request = req c.reset() engine.handleHTTPRequest(c) // 處理 HTTP 請求 engine.pool.Put(c) // 使用完 context 對象, 歸還給 pool }
ServeHTTP會先獲取 Gin Context 上下文信息,接着將 Context 注入到 engine.handleHTTPRequest(c) 方法內來處理 HTTP 請求:
func (engine *Engine) handleHTTPRequest(c *Context) { httpMethod := c.Request.Method rPath := c.Request.URL.Path ... // Find root of the tree for the given HTTP method t := engine.trees for i, tl := 0, len(t); i < tl; i++ { ... root := t[i].root // Find route in tree value := root.getValue(rPath, c.params, unescape) ... if value.handlers != nil { c.handlers = value.handlers c.fullPath = value.fullPath c.Next() // 具體執行響應處理 c.writermem.WriteHeaderNow() return } if httpMethod != "CONNECT" && rPath != "/" { ... } break } ... }
handleHTTPRequest 完成 路由 及 回調 方法的查找,執行 Gin.Context.Next() 調用處理響應。
Gin.Context.Next() 僅有數行代碼:
// Next should be used only inside middleware. // It executes the pending handlers in the chain inside the calling handler. // See example in GitHub. func (c *Context) Next() { c.index++ for c.index < int8(len(c.handlers)) { c.handlers[c.index](c) c.index++ } }
功能是在 Gin 內部中間件中執行 handler 調用,即 r.GET() 中傳入的
func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }
方法生成 HTTP 響應。
到這裏咱們完成了 Gin 的請求和響應的完整流程的源碼走讀,可是咱們有必要對 Gin.Context 有多一些的瞭解。
Gin 的 Context 實現了對 request 和 response 的封裝是 Gin 的核心實現之一,其數據結構以下:
// Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. type Context struct { writermem responseWriter Request *http.Request // HTTP 請求 Writer ResponseWriter // HTTP 響應 Params Params handlers HandlersChain // 關鍵: 數組: 內包含方法集合 index int8 engine *Engine // 關鍵: 引擎 // Keys is a key/value pair exclusively for the context of each request. Keys map[string]interface{} // Errors is a list of errors attached to all the handlers/middlewares who used this context. Errors errorMsgs // Accepted defines a list of manually accepted formats for content negotiation. Accepted []string }
其包含了 Gin 請求及響應的上下文信息和 Engine 指針數據
Gin 官方文檔 幾乎全部的示例都是在講解 Context 的使用方法,可用說研究 Context 源碼對用好 Gin 框架會起到只管重要的做用。
感興趣的朋友能夠自行閱讀 Context.go