在上一篇文章 Gin 源碼學習(三)丨路由是如何構建和匹配的? 中,講解了 Gin 的路由是如何實現的,那麼,當路由成功匹配後,或者匹配失敗後,在 Gin 內部會對其如何處理呢?json
在這一篇文章中,將講解 Gin 對一個 HTTP 請求的具體處理流程是怎樣的。緩存
下面,將對一個請求進入 Gin 的處理範圍後的內容,進行一步步展開,講解 Gin 對請求的處理流程。安全
Go 版本:1.14cookie
Gin 版本:v1.5.0閉包
在上一篇文章中,咱們講到 Gin 其實實現了 Go 自帶函數庫 net/http
庫中的 Handler
接口,而且從實現的源代碼中能夠發現,當一個 HTTP 請求到達 Gin 處理的範圍時,首先是在 Gin 的 Engine
類型中的 ServeHTTP(w http.ResponseWriter, req *http.Request)
方法中對 Gin 保存上下文信息的 gin.Context
進行屬性設置和重置操做,而後纔是使用 engine.handleHTTPRequest(c *Context)
方法來對 HTTP 請求進行處理的,下面,咱們一步一步來對相關源代碼進行分析:併發
// ServeHTTP conforms to the http.Handler interface.
// 符合 http.Handler 接口的約定
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 從對象池中獲取已存在的上下文對象
c := engine.pool.Get().(*Context)
// 重置該上下文對象的 ResponseWriter 屬性
c.writermem.reset(w)
// 設置該上下文對象的 Request 屬性
c.Request = req
// 重置上下文中的其餘屬性信息
c.reset()
// 對請求進行處理
engine.handleHTTPRequest(c)
// 將該上下文對象從新放回對象池中
engine.pool.Put(c)
}
// 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.
// 上下文是 Gin 最重要的部分.
// 它容許咱們在中間件之間傳遞變量, 管理流程, 例如驗證請求的 JSON 並呈現 JSON 響應.
type Context struct {
// 對 net/http 庫中的 ResponseWriter 進行了封裝
writermem responseWriter
// 請求對象
Request *http.Request
// 非 net/http 庫中的 ResponseWriter
// 而是 Gin 用來構建 HTTP 響應的一個接口
Writer ResponseWriter
// 存放請求中的 URI 參數
Params Params
// 存放該請求的處理函數切片, 包括中間件加最終處理函數
handlers HandlersChain
// 用於標記當前執行的處理函數
index int8
// 請求的完整路徑
fullPath string
// Gin 引擎對象
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
// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
// 用於緩存請求的 URL 參數
queryCache url.Values
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
// or PUT body parameters.
// 用於緩存請求體中的參數
formCache url.Values
}
複製代碼
上面源代碼中,展現了 engine.ServeHTTP(w http.ResponseWriter, req *http.Request)
方法的執行過程以及 gin.Context
類型的內部結構,須要注意的是,gin.Context
實現了 Go 的 Context
接口,可是並無對其作併發安全處理,所以,應該避免多個 goroutine 同時訪問同一個 Context,若是存在這種狀況,需使用 gin.Context.Copy()
方法,對 gin.Context
進行復制使用。ide
而且,Gin 使用對象池來存放上下文信息,這是一個很是巧妙的設計思想,由於在 Gin 中,會將請求的許多處理信息存放於 gin.Context
中,而 Go 是一門帶有 GC(垃圾回收)的語言,假如在訪問量較大的場景下,若是不使用對象池來緩衝 gin.Context
對象的話,那麼爲每個請求建立一個 gin.Context
對象,而且在完成請求的處理後,將該 gin.Context
對象交給 GC 去處理,這無疑對 GC 增添了許多壓力。因爲 gin.Context
只是用於保存當前請求的處理信息,用於上下文之間的參數傳遞,屬於徹底能夠複用的對象,所以,使用對象池對其進行存放能夠在必定程度上減小 GC 壓力。函數
下面先來看一下在 engine.ServeHTTP(w http.ResponseWriter, req *http.Request)
方法中對 gin.Context
初始化時設置了什麼樣的初始值:post
const (
// 表示未寫入
noWritten = -1
// 200 狀態碼
defaultStatus = http.StatusOK
)
type responseWriter struct {
// net/http 庫中的 ResponseWriter
http.ResponseWriter
// 響應內容大小
size int
// 響應狀態碼
status int
}
func (w *responseWriter) reset(writer http.ResponseWriter) {
w.ResponseWriter = writer
w.size = noWritten
w.status = defaultStatus
}
func (c *Context) reset() {
c.Writer = &c.writermem
c.Params = c.Params[0:0]
c.handlers = nil
c.index = -1
c.fullPath = ""
c.Keys = nil
c.Errors = c.Errors[0:0]
c.Accepted = nil
c.queryCache = nil
c.formCache = nil
}
複製代碼
這裏須要留意的是 gin.Context.index
的初始值,Gin 經過該值來調用處理函數和判斷當前上下文是否終止。學習
下面,咱們來看 Gin 對請求的處理流程,先來看一下 engine.handleHTTPRequest(c *Context)
方法,該方法在前面的幾篇 Gin 源碼學習的文章中出現過屢次,因此這裏也是一樣,只保留其與當前文章主題相關的源代碼:
func (engine *Engine) handleHTTPRequest(c *Context) {
// 省略...
// Find root of the tree for the given HTTP method
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.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
// 開始對請求執行中間件和處理函數
c.Next()
// 設置響應頭信息
c.writermem.WriteHeaderNow()
return
}
// 省略...
break
}
// 省略...
c.handlers = engine.allNoRoute
// 處理 404 錯誤
serveError(c, http.StatusNotFound, default404Body)
}
複製代碼
在上一篇文章中講到過 Gin 請求路由的匹配是在 root.getValue(path string, po Params, unescape bool)
方法中實現的,而且,當返回的 value
對象中的 handlers
屬性不爲 nil
時,則表示該請求存在處理函數,而後將 value
對象中的處理函數切片集、請求參數以及請求的完整路徑信息存放至該請求的上下文對象中,接着調用 context.Next()
方法,下面來看一下該方法的源代碼:
// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// Next 只能在中間件內部使用
// 它在調用處理程序內的鏈中執行掛起的處理程序
// 相似於遞歸調用或函數裝飾器
func (c *Context) Next() {
// index 初始值爲 -1
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
複製代碼
context.Next()
方法的邏輯比較簡單,其實就是遍歷存放於 Gin 上下文中的中間件/處理函數切片,並調用,在上一篇文章中,咱們也講過,context.handlers
切片中,在有多個 HandlerFunc
的時候,除了最後一個爲該路由的處理函數以外,其他的都爲中間件。
這裏須要注意的點是,該 Next()
方法,在使用的時候,只能在中間件內部使用,也就是說,在平常開發中,該方法只能在本身編寫的中間件中出現,而不能出如今其它地方。
下面,咱們以 gin.Default()
建立 gin.Engine
時添加的兩個默認中間件 Logger
和 Recovery
,並結合一個模擬身份驗證的中間件 Auth
爲例,來對 Gin 中間件的工做流程進行詳細講解,先來看一下 gin.Default()
方法添加的默認中間件 Logger
的相關源代碼:
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
// By default gin.DefaultWriter = os.Stdout.
// Logger 是一箇中間件, 該中間件會將日誌寫入 gin.DefaultWriter.
// 默認的 gin.DefaultWriter 爲 os.Stdout, 即標準輸出流, 控制檯
func Logger() HandlerFunc {
return LoggerWithConfig(LoggerConfig{})
}
// LoggerConfig defines the config for Logger middleware.
// Logger 中間件的相關配置
type LoggerConfig struct {
// Optional. Default value is gin.defaultLogFormatter
// 用於輸出內容的格式化, 默認爲 gin.defaultLogFormatter
Formatter LogFormatter
// Output is a writer where logs are written.
// Optional. Default value is gin.DefaultWriter.
// 日誌輸出對象
Output io.Writer
// SkipPaths is a url path array which logs are not written.
// Optional.
// 忽略日誌輸出的 URL 切片
SkipPaths []string
}
// LoggerWithConfig instance a Logger middleware with config.
// 使用 LoggerConfig 配置的 Logger 中間件
func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
formatter := conf.Formatter
if formatter == nil {
formatter = defaultLogFormatter
}
out := conf.Output
if out == nil {
out = DefaultWriter
}
notlogged := conf.SkipPaths
// 是否輸出至終端
isTerm := true
if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" ||
(!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) {
isTerm = false
}
// 標記忽略日誌的 path
var skip map[string]struct{}
if length := len(notlogged); length > 0 {
skip = make(map[string]struct{}, length)
for _, path := range notlogged {
skip[path] = struct{}{}
}
}
return func(c *Context) {
// Start timer
// 記錄開始時間
start := time.Now()
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
// Process request
// 繼續執行下一個中間件
c.Next()
// Log only when path is not being skipped
// 若是 path 在 skip 中, 則忽略日誌記錄
if _, ok := skip[path]; !ok {
param := LogFormatterParams{
Request: c.Request,
isTerm: isTerm,
Keys: c.Keys,
}
// Stop timer
// 記錄結束時間
param.TimeStamp = time.Now()
// 計算耗時
param.Latency = param.TimeStamp.Sub(start)
// 客戶端 IP
param.ClientIP = c.ClientIP()
// 請求方法
param.Method = c.Request.Method
// 請求狀態碼
param.StatusCode = c.Writer.Status()
// 錯誤信息
param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
// 響應體大小
param.BodySize = c.Writer.Size()
if raw != "" {
path = path + "?" + raw
}
param.Path = path
// 日誌打印
fmt.Fprint(out, formatter(param))
}
}
}
複製代碼
其實中間件也就是裝飾器或閉包,實質上就是一種返回類型爲 HandlerFunc
的函數,通俗地講,就是一種返回函數的函數,目的就是爲了在外層函數中,對內層函數進行裝飾或處理,而後再將被裝飾或處理後的內層函數返回。
因爲 HandlerFunc
函數只能接受一個 gin.Context
參數,所以,在上面源代碼中的 LoggerWithConfig(conf LoggerConfig)
函數中,使用 LoggerConfig
配置,對 HandlerFunc
進行裝飾,並返回。
一樣地,在返回的 HandlerFunc
匿名函數中,首先是記錄進入該中間件時的一些信息,包括時間,而後再調用 context.Next()
方法,掛起當前的處理程序,遞歸去調用後續的中間件,當後續全部中間件和處理函數執行完畢時,再回到此處,若是要記錄該 path
的日誌,則再獲取一次當前的時間,與開始記錄的時間進行計算,便可得出本次請求處理的耗時,再保存其它信息,包括請求 IP 和響應的相關信息等,最後將該請求的日誌進行打印處理,這就是使用 gin.Default()
實例一個 gin.Engine
默認添加的 Logger()
中間件的處理流程。
下面,咱們來看一下,另外一個默認中間件 Recovery()
的相關源代碼:
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
// Recovery 中間件用於捕獲處理流程中出現 panic 的錯誤
// 若是鏈接未斷開, 則返回 500 錯誤響應
func Recovery() HandlerFunc {
// DefaultErrorWriter = os.Stderr
return RecoveryWithWriter(DefaultErrorWriter)
}
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
// 使用傳遞的 out 對 Recovery 中間件進行裝飾
func RecoveryWithWriter(out io.Writer) HandlerFunc {
var logger *log.Logger
if out != nil {
logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
}
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
// 用於標記鏈接是否斷開
var brokenPipe bool
// 從錯誤信息中判斷鏈接是否斷開
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
// 省略, 日誌打印相關...
// If the connection is dead, we can't write a status to it.
if brokenPipe { // 若是鏈接已斷開, 則已經沒法爲其寫入狀態碼
// 添加錯誤信息至上下文中, 用於日誌輸出
c.Error(err.(error)) // nolint: errcheck
// 終止該上下文
c.Abort()
} else { // 鏈接未斷開
// 終止該上下文並寫入 500 錯誤狀態碼
c.AbortWithStatus(http.StatusInternalServerError)
}
}
}()
// 繼續執行下一個中間件
c.Next()
}
}
複製代碼
與 LoggerWithConfig(conf LoggerConfig)
函數同樣,RecoveryWithWriter(out io.Writer)
函數僅爲了對最終返回的中間件 HandlerFunc
函數進行裝飾,在該中間件中,可分爲兩個邏輯塊,一個是 defer
,一個是 Next()
,Next()
與 Logger()
中間件中的 Next()
做用相似,這裏在 defer
中使用 recover()
來捕獲在後續中間件中 panic
的錯誤信息,並對該錯誤信息進行處理。
在該中間件中,首先是判斷當前鏈接是否已中斷,而後是進行相關的日誌處理,最後,若是鏈接已中斷,則直接設置錯誤信息,並終止該上下文,不然,終止該上下文並返回 500 錯誤響應。
下面,咱們來看一下 context.Abort()
方法和 context.AbortWithStatus(code int)
方法的相關源代碼:
// 63
const abortIndex int8 = math.MaxInt8 / 2
// 終止上下文
func (c *Context) Abort() {
c.index = abortIndex
}
// 判斷上下文是否終止
func (c *Context) IsAborted() bool {
return c.index >= abortIndex
}
// 終止上下文並將 code 寫入響應頭中
func (c *Context) AbortWithStatus(code int) {
c.Status(code)
c.Writer.WriteHeaderNow()
c.Abort()
}
複製代碼
context.Abort()
方法將當前上下文的 index
值設置爲 63,用於標誌上下文的終止。
context.AbortWithStatus(code int)
也是終止當前的上下文,只不過額外的使用了 code
參數,對響應的頭信息進行了設置。
最後,咱們再來看一個模擬身份校驗的中間件 Auth
,其實現的相關源代碼以下:
type RequestData struct{
Action string `json:"action"`
UserID int `json:"user_id"`
}
func main() {
router := gin.Default()
// 註冊中間件
router.Use(Auth())
router.POST("/action", func(c *gin.Context) {
var RequestData RequestData
if err := c.BindJSON(&RequestData); err == nil {
c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "success"})
}
})
router.Run(":8000")
}
func Auth() gin.HandlerFunc {
// TODO: 可模仿 Logger() 或 Recovery() 中間件, 結合該函數的調用參數, 在此處作一些配置操做
return func(c *gin.Context) {
// TODO: 可模仿 Logger() 中間件, 在此處對請求的 path 進行忽略處理
if auth(c.Request) {
c.Next()
} else {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "Unauthorized"})
}
}
}
func auth(req *http.Request) bool {
// TODO: 對 http.Request 中的信息進行校驗, 如 cookie, token...
return req.Header.Get("Auth") == "colelie"
}
複製代碼
首先是 Auth()
函數,該函數用於裝飾並返回 gin.HandlerFunc
函數,在該函數內,返回了一個 gin.HandlerFunc
匿名函數,在該匿名函數中,經過調用 auth(req *http.Request)
函數對請求信息進行校驗,這裏只是一個簡單地對請求頭中的 Auth
進行驗證。
因此,在該案例中,當咱們訪問 /action
接口時,首先會進入 Logger()
中間件,而後進入 Recovery()
中間件,再進入 Auth()
中間件,當前面的中間件都沒有發生對上下文的終止操做時,纔會進入咱們聲明的 router.POST("/action", func)
處理函數。
當咱們向 /action
接口發起一個普通的 POST 請求時,會收到以下響應:
這是因爲在 Auth()
中間件中身份校驗沒經過,咱們爲該請求的頭部信息中添加一個 Key 爲 Auth
,Value 爲 colelie
的字段,會收到以下響應:
能夠發現,一樣的,出現了錯誤響應,而此次的錯誤響應碼爲 400,這是爲何呢?
在 Gin 源碼學習(二)丨請求體中的參數是如何解析的? 中,咱們講過,在使用 MustBind
一類的綁定函數時,若是在參數解析過程當中出現錯誤,會調用 c.AbortWithError(http.StatusBadRequest, err)
方法,終止當前的上下文並返回 400 響應錯誤碼,在上面的聲明的對 /action
的處理函數中,使用了 context.BindJSON(obj interface{})
方法對請求參數進行綁定操做,下面,咱們在爲請求添加可以綁定成功的請求體,會收到以下響應:
此次,獲得了正確的響應內容。
在這篇文章中,咱們圍繞 gin.Context
的內部結構、Gin 中間件和處理函數的工做流程,講解了 Gin 對請求的處理流程。
首先,在 gin.Engine
中,使用對象池 sync.Pool
來存放 gin.Context
這樣作的目的是爲 Go GC 減小壓力。
而後,在 Gin 內部,當路由匹配成功後,將調用 context.Next()
方法,開始進入 Gin 中間件和處理函數的執行操做,而且,須要注意的是,在平常開發中,該方法,只能在中間件中被調用。
最後,以使用 gin.Default()
方法建立 gin.Engine
時攜帶的兩個默認中間件 Logger()
和 Recovery()
,和咱們本身編寫的一個模擬身份校驗的中間件 Auth()
,結合註冊的 path 爲 /action
的路由,對 Gin 中間件和處理函數的工做流程進行了講解。
至此,Gin 源碼學習的第四篇也就到此結束了,感謝你們對本文的閱讀~~
歡迎掃描如下二維碼關注筆者的我的訂閱號,獲取最新文章推送: