Gin 的啟動過程、路由及上下文源碼解讀

Engine

Engine 是 gin 框架的一個實例,它包含了多路復(fù)用器、中間件和配置中心。

Engine 的啟動

gin 通過 Engine.Run(addr ...string) 來啟動服務(wù),最終調(diào)用的是 http.ListenAndServe(address, engine),其中第二個參數(shù)應(yīng)當(dāng)是一個 Handler 接口的實現(xiàn),即 engine 實現(xiàn)了此接口:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Engine.ServeHTTP() 會先初始化一個空的上下文,然后掛上請求 c.Reuqest = req,隨后執(zhí)行 engine.handlerHTTPRequest(c)(包含主要處理邏輯的函數(shù))。

  • engine.handlerHTTPRequest() 會先設(shè)置處理一些配置項,如 UseRawPath、RemoveExtraSlash 等
  • 然后開始尋找路由,從 engine.trees 中尋找
  • 當(dāng)找到后執(zhí)行找到路由對應(yīng)的處理鏈 .handlers

以上就是正常處理一個請求的主要邏輯,其他的就現(xiàn)階段來說先忽略了。

RouterGroup

Engine 組合了 RouterGroup。

RouterGroup 實現(xiàn)了 IRouter 接口,IRouter 接口是 IRoutes 接口和 Group 函數(shù)組合而成。

  • IRoutes 接口定義了所有路由處理的實現(xiàn)方法。
  • IRouter 接口定義了所有路由處理的實現(xiàn)方法以及一個分組方法(Group())。
// IRouter defines all router handle interface includes single and group router.
type IRouter interface {
    IRoutes
    Group(string, ...HandlerFunc) *RouterGroup
}

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

    StaticFile(string, string) IRoutes
    Static(string, string) IRoutes
    StaticFS(string, http.FileSystem) IRoutes
}

RouterGroup 的結(jié)構(gòu)

RouterGroup 的結(jié)構(gòu)體只有四個屬性:

type RouterGroup struct {
    Handlers HandlersChain // 創(chuàng)建時候會從父親那 copy 一份,然后 append 指定的 handlers
    basePath string // 創(chuàng)建時候會從父親那得到前綴,然后拼接指定的相對地址
    engine   *Engine // 會永遠(yuǎn)引用引擎
    root     bool // 標(biāo)記位
}
  • .Handlers 屬性是一個切片,按序存儲著處理函數(shù)(中間件方法和最終的處理函數(shù))
  • .basePath 屬性是定位此 Group 的地址路徑
  • .engine 屬性總是指向 Engine 實例,且父子 Group 都存有相同的引用
  • .root 屬性標(biāo)記了此 Group 是否為根組,即最祖先的結(jié)點

當(dāng)新建 Engine 時,會初始化一個 RouterGroup 結(jié)構(gòu),RouterGroup 是組合在 Engine 中的(所以 Engine 可以調(diào)用 RouterGroup 的所有方法),同時 Engine 的引用也記錄在了 RouterGroup 上。

函數(shù)實現(xiàn)

如上,RouterGroup 實現(xiàn)了 IRouter 接口,下面是一些方法的實現(xiàn)。

  • Group() 方法,RouterGroup 通過 Group() 方法創(chuàng)建子分組,子分組會繼承下來父 Group 的 handlers 并追加自己獨(dú)有的 handlers,計算出此 Group 的 path 地址,及記錄 Engine 地址。
  • POST(relativePath string, handlers ...HandlerFunc) 調(diào)用了 handle() 方法,是在 Group 中加一個路由(相對地址)及處理函數(shù)鏈(很常用就不多說了,其他類似方法也略)。
  • Handle(method, relativePath string, handlers ...HandlerFunc) 方法相對于 POST()/GET() 等方法只是可以傳入自定義的方法名,用于特殊的、不標(biāo)準(zhǔn)的、Gin 內(nèi)置不存在的的請求方法(不常用)。
  • Any() 會將路由及函數(shù)處理鏈在所有的支持方法上都 copy 存儲一份,以實現(xiàn)通過任何請求方法都會有同樣的調(diào)用鏈。
  • StaticFile(relativePath, filePath) 會將路由映射到文件系統(tǒng)的某一文件上,此時的 relativePath 是不允許有變量存在的(不允許有 : 和 * )。內(nèi)部通過 c.File() 響應(yīng)此文件。
  • Static(relativePath, root string) 將路由映射到文件系統(tǒng)的某一個文件夾上,底層調(diào)用了 StaticFS(relativePath, Dir(root, false))
  • StaticFS() 類似 Static(),但自定義 http.FileSystem 了,F(xiàn)ileSystem 就可以理解為一個目錄,這個目錄就是所謂的文件系統(tǒng)。gin 的實現(xiàn)為了安全禁用了目錄中的 list 功能。

StaticX 方法都加了路徑中不允許存在變量(:、*)的判斷,所以使用是安全的。
var _ IRouter = &RouterGroup{} 可以用來檢查 RouterGroup 是否實現(xiàn)了 IRouter 接口。??

擴(kuò)展:從 gin 對于 FileSystem 的實現(xiàn)可以探索更底層的東西。
gin.Dir(root string, listDirectory bool) 實現(xiàn)了對 http.Dir(root string) 的封裝。
http.Dir() 用了本地文件系統(tǒng)的目錄樹,直接對外暴露一個文件夾有時候是不安全。比如文件中有些關(guān)鍵的隱藏文件等情況。
gin.Dir() 的第二個參數(shù)控制是否可以顯示文件系統(tǒng)下的文件列表,默認(rèn) false 不顯示,相對比較安全。
通過看源碼發(fā)現(xiàn) gin 是通過 onlyFilesFS.Readdir() 函數(shù)重寫了 Readdir() 函數(shù)實現(xiàn)關(guān)閉 list 文件的。

Route 的添加

gin 通過上方 RouterGroup 暴露的幾個方法添加路由,底層使用的方法是 Engine.addRoute(method, path string, handlers HandlerChain)。

Engine.trees 屬性是存儲所有路由信息的總?cè)肟?。它是一個切片,其中每個元素對應(yīng)一種 method 并且是一個多叉樹的根節(jié)點。

  • Engine.trees 是一個數(shù)組 []methodTree
  • methodTree{method string, root *node}
  • node{} 是個結(jié)點

當(dāng) addRoute 時,先根據(jù) method 找到對應(yīng)的 tree (Engine.trees[i])。然后會比較 加入者 的 path 和 node 中的 path 相似的部分,相似的部分 作為 父結(jié)點,不同的部分作為 子結(jié)點。以 多叉樹 的方式存儲下來。

這里會把 URL 中的路由變量也當(dāng)作字符串存入樹中,因為相同 URL 他們的變量也是一樣的。

Route 的匹配

當(dāng)請求進(jìn)來時,因為 Engine 實現(xiàn)了 Handler 接口,所以最后會調(diào)用到 Engine.ServeHTTP 內(nèi)。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

gin 的 ServeHTTP 源碼中可以看到獲取它的 gin.Context 是通過池實現(xiàn)的,獲取之后重置 ctx 中的信息。

找路徑在

func (engine *Engine) handleHTTPRequest(c *Context) {
    // ...
    value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
    // ...
}

root.getValue() 比較復(fù)雜,這里就不多解釋了。

gin 用的是 julienschmidt/httprouter 庫的支持,所以可以參考這里。

Context

gin@v1.7.7 context.go

Context 中定義了一些屬性和方法,用于擴(kuò)展一些功能。

創(chuàng)建類方法

可以看到,這些方法主要用來獲取 gin 自身 Context 的一些信息。

  • Copy() ,復(fù)制一個 Context 用于 goroutine 使用
  • HanderName() 方法,通過反射實現(xiàn)的獲取 handler 的名字(以 包路徑.NAME 的形式)
  • HandlerNames() 方法,返回完整的 handlers 鏈(這個真是相見恨晚的方法,可以用來調(diào)試一些調(diào)試中間件的一些異常問題,尤其是中間件中的數(shù)據(jù)傳遞問題)
  • Handler() 方法,返回主處理方法(最后一個)
  • FullPath() 方法,返回路由的 URL 全路徑

HanderName() 的主要實現(xiàn)是通過反射方法獲取到函數(shù)的名稱:
runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()

reflect.ValueOf(f).Pointer() 返回 f 的 uintptr 值
runtime.FuncForPC() 將 PC(program counter,程序計數(shù)器地址)解釋為 *Func 類型,即 Go 中函數(shù)在運(yùn)行時的二進(jìn)制表示
*Func 上有三個函數(shù)

  • Name() 返回函數(shù)全名(包地址+函數(shù)名)
  • Entry() 返回函數(shù)的 uintptr 地址
  • FileLine(pc uintptr) 返回 pc 指針的文件名所在行號

流程控制類方法

Context 中保存了所有 handlers 列表,存在 Context.handlers 數(shù)組中,并用下標(biāo) Context.index 標(biāo)記當(dāng)前執(zhí)行的位置。
當(dāng)主動取消調(diào)用鏈時,會將 index 設(shè)置成一個最大值 63(math.MaxInt8 / 2),也即調(diào)用鏈最大支持 64 個函數(shù)。
Context 中還提供了其他一些函數(shù),當(dāng)取消調(diào)用鏈的時候,可以設(shè)置請求返回的狀態(tài)碼和返回數(shù)據(jù)信息等。

  • Next() 方法,用于中間件中才有意義,用于在當(dāng)前函數(shù)中開始執(zhí)行下一個 handler
  • IsAborted() 方法,判斷當(dāng)前上下文是否已經(jīng)取消
  • Abort() 方法,設(shè)置調(diào)用鏈為取消狀態(tài)。
  • AbortWithError() 方法,= AbortWithStatus() + Error()
  • AbortWithStatus() 取消調(diào)用臉并設(shè)置 http 狀態(tài)碼
  • AbortWithStatusJSON() 方法,= Abort() + JSON()

Context 中的 httpWriter 整理一下。

錯誤處理

gin 在 Context 中定義了錯誤信息字段 Context.Errors 切片,可以鏈?zhǔn)酱鎯﹀e誤信息。

  • Error(err error) *Error 用于將 err 追加到錯誤信息列表中。

元數(shù)據(jù)管理

Go 原生的 Context 是通過 ValueContext 來存儲元數(shù)據(jù)信息的,每個 ValueContext 只能存儲一對信息,存儲多個信息對需要將許多 ValueContext 組成鏈條,讀寫很不高效。
gin 的 Context 中存的元數(shù)據(jù)數(shù)據(jù)是存在 Context.Keys map[string]interface{} 屬性中的,比起原生的 Context 使用起來會更高效。

  • Set(key string, value interface{}) 設(shè)置鍵值對,存儲到 Keys 屬性中。
  • gin.Context 提供了比較豐富的獲取各種類型數(shù)據(jù)的方法,如 GetString、GetTime、GetStringSlice、GetStringMapStringSlice 等等。

元數(shù)據(jù)的讀和寫是并發(fā)安全的。
重復(fù)設(shè)置某一個 key,會更新存儲的 value。

輸入數(shù)據(jù)

Param 類

是指用在 URL 路徑中設(shè)置的參數(shù),如 /user/:id 的 id 參數(shù)。
存儲在 Context.Params 屬性中,其本質(zhì)是一個切片,每一個元素是一個 K/V 元組。
因此,在 URL 中是可以使用重復(fù)的變量名的(如 /test/:id/case/:id),但獲取值就需要自己從屬性中獲取了(如:c.Params[0])。

  • Param(key) 用于獲取單個 URL 內(nèi)的參數(shù)。

解析請求中 URL Params 參數(shù)的位置是在 Engine.handleHTTPRequest()

Query 類

Query 類是用在 URL 后的參數(shù)部分(如:?id=1)。

gin 通過 Context.queryCache 屬性存儲 query 參數(shù),在調(diào)用獲取 Query 參數(shù)時以懶加載的方式初始化:c.queryCache = c.Request.URL.Query()。

需要注意的是它也支持傳入 map 和 array,map 的傳入需要像這樣 ?m[k1]=v1&m[k2]=v2,array 的傳入像這樣 ?a=1&a=2

  • Query(key),按 key 獲取字符串參數(shù)值
  • DefaultQuery(key, defaultVal),獲取字符串,當(dāng)沒有傳時返回默認(rèn)值。值得注意的是,這里“當(dāng)沒有傳時”不包含傳了但值為空的情況。即當(dāng)傳 ?a=&b=時,會取到空值而不是默認(rèn)值。
  • QueryTYPE(),獲取指定類型的數(shù)據(jù),TYPE是個占位符,若無則返回空值。Type 可以是 Array、Map
  • GetQueryTYPE(key),比起 QueryType,這類函數(shù)會返回第二個參數(shù)表明參數(shù)中有沒有設(shè)置

Form 類

包含 PostForm、FormFile、MultipartForm 等。
先略

  • FormFile() 獲取用戶提交的表單中的文件。
  • SaveUploadedFile() 將用戶表單提交的文件保存到服務(wù)器文件系統(tǒng)中。
  • MultipartForm()

綁定引擎

gin 為方便使用,通過綁定引擎設(shè)置了自動綁定用戶輸入和結(jié)構(gòu)數(shù)據(jù)的方法。

  • Bind() 按 Content-Type 自動綁定結(jié)構(gòu)數(shù)據(jù)。支持類型 JSON / XML / YAML / Form-Data / ProtoBuf / MsgPack(見binding 包的 Default() 函數(shù))。
  • BindJSON/BindXML/BindYAML/... 指定各自類型的綁定。
  • BindHeader() 綁定 header
  • BindUri 綁定 URL path 內(nèi)的參數(shù)到結(jié)構(gòu)體中

響應(yīng)渲染

這里包含設(shè)置狀態(tài)碼、設(shè)置響應(yīng)頭以及等信息。

只說一些值得注意的

  • Header(k, v string) 設(shè)置響應(yīng)的 header,當(dāng)值為空字符串時,相當(dāng)于刪除此 header
  • IndentedJSON() 一般不要在正式環(huán)境中用,因為輸出格式化的 JSON 是很耗 CPU 的事。
  • JSONP() xxx
  • DataFromReader() 由 gin 從 io 中讀數(shù)據(jù)并寫到響應(yīng)
  • File() 高效率地寫一個文件到響應(yīng)。調(diào)用的是 http.ServeFile(),會禁用包含 .. 的路徑
  • FileFromFS() 指定 FileSystem 響應(yīng)文件內(nèi)容
  • FileAttachment() 用指定的文件名響應(yīng)客戶端下載附件。
  • SSEvent() 寫一個 Server-Send 事件到信息流中。
  • Stream() 流式響應(yīng)

內(nèi)容協(xié)商

  • Negotiate() 根據(jù) Accept 的格式調(diào)用不同的 Render 方法
  • NegotiateFormat() 返回可接受到格式
  • SetAccepted() 設(shè)置 Accept header

實現(xiàn) context.Context 接口

這些方法除了 .Value() 方法外,其他都是返回的默認(rèn)空值,略。

其他

  • ClientIP() 返回用戶 IP 地址,先獲取 RemoteIP 然后從 Header 中獲取真實 IP
  • RemoteIP() 返回 RemoteAddr
  • IsWebsocket() 通過 Header 判斷是否為 websocket 請求
  • GetRawData() 獲取流數(shù)據(jù)(c.Request.Body)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容