深入淺出 一文帶你了解Gin 生命周期

Gin 是一個用 Go (Golang) 編寫的 web 框架傍药,由于出色的性能優(yōu)勢而被廣泛使用,這里我們就來分析下 Gin 的請求生命周期

1 Gin 目錄結(jié)構(gòu)

先來了解下其目錄結(jié)構(gòu):

.├──binding依據(jù)HTTP請求Accept解析響應(yīng)數(shù)據(jù)格式│?? ├──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依據(jù)解析的HTTP請求Accept響應(yīng)格式生成響應(yīng)│?? ├──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設(shè)置Gin運行環(huán)境模式├──path.goPath處理├──recovery.go處理Panic的Recovery中間件├── *response_writer.goResponseWriter├── *routergroup.go路由組設(shè)置├──tree.go路由算法├──utils.gohelper函數(shù)└──version.go

其中比較重要的模塊為: context.go靴姿,gin.go沃但,routergroup.go,以及 tree.go佛吓;分別處理 HTTP 請求及響應(yīng)上下文宵晚,gin 引擎初始化垂攘,路由注冊及路由查找算法實現(xiàn)。

binding 目錄內(nèi)提供基于 HTTP 請求消息頭 Context-Type 的 MIME 信息自動解析功能淤刃,相對應(yīng)的 Render 目錄下提供具體數(shù)據(jù)格式渲染的實現(xiàn)方法晒他。

2 Gin 請求生命周期

本文著重介紹 Gin 實現(xiàn)一個 Web 服務(wù)器,從請求到達到生成響應(yīng)整個生命周期內(nèi)的核心功能點逸贾,這將有助于我們理解 Gin 的執(zhí)行原理和以后的開發(fā)工作的展開陨仅。

2.1 簡單了解下 Gin 服務(wù)執(zhí)行流程

先從官網(wǎng)的第一個 demo example.go 出發(fā):

packagemainimport"github.com/gin-gonic/gin"funcmain(){// 創(chuàng)建 Gin Engine 實例r := gin.Default()// 設(shè)置請求 URI /ping 的路由及響應(yīng)處理函數(shù)r.GET("/ping",func(c *gin.Context){? ? ? ? c.JSON(200, gin.H{"message":"pong",? ? ? ? })? ? })// 啟動 Web 服務(wù),監(jiān)聽端口,等待 HTTP 請求到并生成響應(yīng)r.Run()// 監(jiān)聽并在 0.0.0.0:8080 上啟動服務(wù)}

通過執(zhí)行 go run example.go 命令來運行代碼铝侵,它會啟動一個阻塞進程監(jiān)聽并等待 HTTP 請求:

# 運行 example.go并且在瀏覽器中訪問0.0.0.0:8080/ping$gorun example.go

從代碼中我們可以看出通過 Gin 實現(xiàn)一個最簡單的 Web 服務(wù)器灼伤,只需 3 個步驟:

1)創(chuàng)建 Gin 實例

2)注冊路由及處理函數(shù)

3)啟動 Web 服務(wù)

2.2 Gin 生命周期

2.2.1 創(chuàng)建 Gin 實例

Gin 實例創(chuàng)建通過?gin.Default()?方法完成,其定義在 gin.go#L159 文件里:

// Default returns an Engine instancewiththe LoggerandRecoverymiddleware already attached.funcDefault() *Engine{? ? debugPrintWARNINGDefault()engine:=New()? ? engine.Use(Logger(),Recovery())returnengine}

Default()?方法實現(xiàn)如下功能:

1)創(chuàng)建 Gin 框架對象 Engine

2)配置 Gin 默認的中間件咪鲜,Logger() 和 Recovery()狐赡,其實現(xiàn)分別位于 logger.go 和 recovery.go 文件內(nèi)

3)返回 Gin 框架對象

其中?New()?方法會實例化 Gin 的 Engine 對象,gin.go#L129

//NewreturnsanewblankEngineinstancewithoutanymiddlewareattached.//By default the configuration is://-RedirectTrailingSlash:true//-RedirectFixedPath:false//-HandleMethodNotAllowed:false//-ForwardedByClientIP:true//-UseRawPath:false//-UnescapePathValues:truefuncNew()*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=engineengine.pool.New=func()interface{}{returnengine.allocateContext()}returnengine}

實例化比較核心的功能是:

1)初始化 Engine 對象, 關(guān)鍵步驟是初始化路由組 RouterGroup疟丙。

2)初始化 pool, 這是核心步驟. pool 用來存儲 context 上下文對象. 用來優(yōu)化處理 http 請求時的性能猾警。

后面會重點分析 engine.pool 的實現(xiàn)細節(jié)。

Engine?是 Gin 框架的核心引擎 gin.go#L56隆敢,數(shù)據(jù)結(jié)構(gòu)如下:

typeEngine struct {RouterGroup// 關(guān)鍵:路由組//設(shè)置開關(guān)RedirectTrailingSlashboolRedirectFixedPathboolHandleMethodNotAllowedboolForwardedByClientIPboolAppEngineboolUseRawPathboolUnescapePathValuesboolMaxMultipartMemoryint64RemoveExtraSlashbool//界定符delimsrender.DelimssecureJSONPrefixstringHTMLRenderrender.HTMLRenderFuncMaptemplate.FuncMapallNoRouteHandlersChainallNoMethodHandlersChainnoRouteHandlersChainnoMethodHandlersChainpoolsync.Pool // 關(guān)鍵:context 處理treesmethodTreesmaxParamsuint16}

Engine 結(jié)構(gòu)體內(nèi)部除一些功能性開關(guān)設(shè)置外发皿,核心的就是 RouterRroup,pool 和 trees拂蝎。Gin 的所有組件都是由 Engine 驅(qū)動穴墅。

2.2.2 路由注冊

完成 Gin 的實例化之后,我們可以通過?r.GET("/ping", func(c *gin.Context) {})?定義 HTTP 路由及處理 handler 函數(shù)温自。

以 gin.GET 為例玄货,展開源碼如下:

// GET is a shortcut for router.Handle("GET", path, handle).func(group *RouterGroup)GET(relativePathstring, handlers ...HandlerFunc)IRoutes{returngroup.handle(http.MethodGet, relativePath, handlers)}

gin.GET?定義 HTTP GET 請求的路由及處理方法,并返回?IRoutes?對象實例悼泌。

2.2.2.1 RouterGroup 結(jié)構(gòu)體

// RouterGroup is used internally to configure router, a RouterGroup is associated with// a prefix and an array of handlers (middleware).typeRouterGroupstruct{Handlers HandlersChain? ? basePath string? ? engine? *Engine? ? rootbool}

RouterGroup?routergroup.go#L41 用于配置路由松捉,其中:

Handlers 數(shù)組定義了 Gin 中間件調(diào)用的 handler 方法

engine 為 gin.go 實例化時設(shè)置的 Engine 實例對象

2.2.2.2 handle 添加路由

在?gin.GET?方法內(nèi)部通過調(diào)用?group.handle()?routergroup.go#L72 方法添加路由:

func(group*RouterGroup)handle(httpMethod,relativePathstring,handlersHandlersChain)IRoutes{absolutePath := group.calculateAbsolutePath(relativePath)? ? handlers = group.combineHandlers(handlers)? ? ? ? group.engine.addRoute(httpMethod, absolutePath, handlers)// 添加路由? ? return group.returnObj()}

路由就和 Engine 綁定好關(guān)系了。

2.2.2.3 IRoute 接口類型

// IRoutes defines all router handle interface.typeIRoutesinterface{? ? 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 實現(xiàn)了這個接口。

2.2.2.4 小結(jié)

推而廣之鸠踪,Gin 還支持如下等路由注冊方法:

r.POST

r.DELETE

r.PATCH

r.PUT

r.OPTIONS

r.HEAD

以及 r.Any

它們都定義在 routergroup.go 文件內(nèi)丙者。

2.2.3 接收請求并響應(yīng)

Gin 實例化和路由設(shè)置后工作完成后,我們進入 Gin 生命周期執(zhí)行的核心功能分析营密,Gin 究竟是如何啟動 Web 服務(wù)械媒,監(jiān)聽 HTTP 請求并執(zhí)行 HTTP 請求處理函數(shù)生成響應(yīng)的。這些工作統(tǒng)統(tǒng)從?gin.Run()?出發(fā) 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){deferfunc(){ 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 啟動并監(jiān)聽 HTTP 請求纷捞。

由此痢虹,我們不得不放下手頭的工作,率先了解下 net/http 標準庫的執(zhí)行邏輯主儡。

2.2.3.1 net/http 標準庫

net/http 標準庫的?ListenAndServe(addr string, handler Handler)?方法定義在 net/http/server.go#L3162 文件里奖唯。

參數(shù)簽名的第一個參數(shù)是監(jiān)聽的服務(wù)地址和端口;

第二個參數(shù)接收一個 Handler 對象它是一個接口類型需要實現(xiàn)?ServeHTTP(ResponseWriter, *Request)?方法缀辩。

func ListenAndServe(addr string,handlerHandler)error{server:= &Server{Addr: addr,Handler:handler}returnserver.ListenAndServe()}

在?ListenAndServe(addr string, handler Handler)?內(nèi)部則調(diào)用的是 Server 對象的?ListenAndServe()?方法由交由它啟動監(jiān)聽和服務(wù)功能:

// 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{ifsrv.shuttingDown() {returnErrServerClosed? ? }? ? addr := srv.Addrifaddr ==""{? ? ? ? addr =":http"}? ? ln, err := net.Listen("tcp", addr)// 監(jiān)聽iferr !=nil{returnerr? ? }returnsrv.Serve(ln)// 啟動服務(wù)等待連接}

然后,執(zhí)行?srv.Serve(ln)?即?Server.Serve(l net.Listener)?server.go#L2951踪央,在?net.Listen("tcp", addr)?等待連接臀玄,創(chuàng)建新的 goroutine 來處理請求和生成響應(yīng)的業(yè)務(wù)邏輯:

func(srv *Server)Serve(l net.Listener)error{? ? ...for{? ? ? ? rw, e := l.Accept()ife !=nil{select{case<-srv.getDoneChan():returnErrServerCloseddefault:? ? ? ? ? ? }? ? ? ? ? ? ...returne? ? ? ? }ifcc := srv.ConnContext; cc !=nil{? ? ? ? ? ? ctx = cc(ctx, rw)ifctx ==nil{panic("ConnContext returned nil")? ? ? ? ? ? }? ? ? ? }? ? ? ? tempDelay =0c := srv.newConn(rw)? ? ? ? c.setState(c.rwc, StateNew)// before Serve can returngoc.serve(ctx)// 啟動 Web 服務(wù)}}

最后,進入到?go c.serve(ctx)?啟動 Web 服務(wù)畅蹂,讀取 HTTP 請求數(shù)據(jù)健无,生成響應(yīng) 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)? ? ? ? ...? ? }}

最終,調(diào)用?r.Run()?方法傳入的 Engine 來執(zhí)行?serverHandler{c.server}.ServeHTTP(w, w.req)?處理接收到的 HTTP 請求和生成響應(yīng)液斜,這里將響應(yīng)處理的控制權(quán)交回給 Gin Engine累贤。

小結(jié)

Go 標準庫 net/http 提供了豐富的 Web 編程接口支持,感興趣的朋友可以深入研究下 net/http 標準庫源碼少漆,了解其實現(xiàn)細節(jié)臼膏。

2.2.3.2 Engine.ServeHTTP 處理 HTTP 請求

Engine.ServeHTTP?是 Gin 框架核心中的核心,我們來看下它是如何處理請求和響應(yīng)的:

// 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= reqc.reset()? ? engine.handleHTTPRequest(c)// 處理 HTTP 請求engine.pool.Put(c)// 使用完 context 對象, 歸還給 pool }

ServeHTTP會先獲取 Gin Context 上下文信息示损,接著將 Context 注入到 engine.handleHTTPRequest(c) 方法內(nèi)來處理 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 methodt := engine.treesfori, tl :=0,len(t); i < tl; i++ {? ? ? ? ...? ? ? ? root := t[i].root// Find route in treevalue := root.getValue(rPath, c.params, unescape)? ? ? ? ? ? ? ? ...ifvalue.handlers !=nil{? ? ? ? ? ? c.handlers = value.handlers? ? ? ? ? ? c.fullPath = value.fullPath? ? ? ? ? ? c.Next()// 具體執(zhí)行響應(yīng)處理c.writermem.WriteHeaderNow()return}ifhttpMethod !="CONNECT"&& rPath !="/"{? ? ? ? ? ? ...? ? ? ? }break}? ? ...}

handleHTTPRequest?完成?路由?及?回調(diào)?方法的查找渗磅,執(zhí)行 Gin.Context.Next() 調(diào)用處理響應(yīng)。

2.2.3.3 Gin.Context.Next() 在內(nèi)部中間件執(zhí)行 handler 方法

Gin.Context.Next()?僅有數(shù)行代碼:

// 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++forc.index < int8(len(c.handlers)) {c.handlers[c.index](c)c.index++? ? }}

功能是在 Gin 內(nèi)部中間件中執(zhí)行 handler 調(diào)用检访,即 r.GET() 中傳入的

func(c*gin.Context){c.JSON(200, gin.H{"message":"pong",? ? })}

方法生成 HTTP 響應(yīng)始鱼。

到這里我們完成了 Gin 的請求和響應(yīng)的完整流程的源碼走讀,但是我們有必要對 Gin.Context 有多一些的了解脆贵。

2.2.3.4 Gin.Context 上下文處理

Gin 的 Context 實現(xiàn)了對 request 和 response 的封裝是 Gin 的核心實現(xiàn)之一医清,其數(shù)據(jù)結(jié)構(gòu)如下:

// 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.typeContextstruct{? ? writermem responseWriter? ? Request? *http.Request// HTTP 請求Writer? ? ResponseWriter// HTTP 響應(yīng)Params? Params? ? handlers HandlersChain// 關(guān)鍵: 數(shù)組: 內(nèi)包含方法集合indexint8engine *Engine// 關(guān)鍵: 引擎// Keys is a key/value pair exclusively for the context of each request.Keysmap[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 請求及響應(yīng)的上下文信息和 Engine 指針數(shù)據(jù)

Request *http.Request : HTTP 請求

Writer ResponseWriter : HTTP 響應(yīng)

handlers HandlersChain : 是?type HandlerFunc func(*Context)?方法集即路由設(shè)置的回調(diào)函數(shù)

engine *Engine : gin框架對象

Gin 官方文檔 幾乎所有的示例都是在講解 Context 的使用方法,可用說研究 Context 源碼對用好 Gin 框架會起到只管重要的作用卖氨。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末会烙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子筒捺,更是在濱河造成了極大的恐慌持搜,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件焙矛,死亡現(xiàn)場離奇詭異葫盼,居然都是意外死亡,警方通過查閱死者的電腦和手機村斟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門贫导,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抛猫,“玉大人,你說我怎么就攤上這事孩灯」虢穑” “怎么了?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵峰档,是天一觀的道長败匹。 經(jīng)常有香客問我,道長讥巡,這世上最難降的妖魔是什么掀亩? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮欢顷,結(jié)果婚禮上槽棍,老公的妹妹穿的比我還像新娘。我一直安慰自己抬驴,他們只是感情好炼七,可當我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著布持,像睡著了一般豌拙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上题暖,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天姆蘸,我揣著相機與錄音,去河邊找鬼芙委。 笑死逞敷,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的灌侣。 我是一名探鬼主播竭望,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼绿聘,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蒸播,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤驻售,失蹤者是張志新(化名)和其女友劉穎嗦明,沒想到半個月后撩荣,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡哪审,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年蛾魄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡滴须,死狀恐怖舌狗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情扔水,我是刑警寧澤痛侍,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站魔市,受9級特大地震影響主届,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜待德,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一君丁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧磅网,春花似錦谈截、人聲如沸筷屡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽毙死。三九已至燎潮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扼倘,已是汗流浹背确封。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留再菊,地道東北人爪喘。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像纠拔,于是被迫代替她去往敵國和親秉剑。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容