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)