我給 gin 提交了一行代碼

這篇文章記錄一次給 gin-gonic/gin[1] 提交了一行代碼的經歷,雖然沒什麼含金量,但是對我而言還是挺開心的哈哈。

緣由

事情是這樣的,gin 默認的 404 頁面返回的是 404 page not found ,我們項目中需要自定義該頁面進行跳轉,第一直覺肯定是 gin 會有相應的 API ,事實如此,gin 有一個 NoRoute 方法可以自定義 404 頁面的 handler ,它的源碼如下:

// NoRoute adds handlers for NoRoute. It return a 404 code by default.
func (engine *Engine) NoRoute(handlers ...HandlerFunc) {
 engine.noRoute = handlers
 engine.rebuild404Handlers()
}

後面我還想要路由找不到對應的 Method 時也進行自定義處理,習慣性的翻看了對應的 API 源碼:

// NoMethod sets the handlers called when... TODO.
func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
 engine.noMethod = handlers
 engine.rebuild405Handlers()
}

發現 NoMethod 方法註釋竟然是被標記爲 TODO ,此時我還沒去細究,想着先寫個簡單例子測試一下:

package main

import (
 "fmt"
 "github.com/gin-gonic/gin"
 "net/http"
)

func main() {
 r := gin.Default()
 r.NoRoute(func(c *gin.Context) {
  c.String(http.StatusOK, "NoRoute")
 })
 r.NoMethod(func(c *gin.Context) {
  fmt.Println("NoMethod")
  c.String(http.StatusOK, "NoMethod")
 })
 r.POST("/", func(c *gin.Context) {
  c.String(http.StatusOK, "/")
 })
 r.Run(":8080")
}

這段代碼很簡單,我的預期結果應該是這樣的:

$ curl http://localhost:8080
NoMethod

$ curl -X POST http://localhost:8080
/

$ curl -X PUT http://localhost:8080
NoMethod

但實際上,它卻是這樣的:

$ curl http://localhost:8080
NoRoute

$ curl -X POST http://localhost:8080
/

$ curl -X PUT http://localhost:8080
NoRoute

這說明 NoMethod 並沒有生效,此時以爲難道真的是因爲處於 TODO 狀態還未實現嗎,所以開始了深入研究其源碼來解開我的疑惑。

源碼分析

// NoMethod sets the handlers called when... TODO.
func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
 engine.noMethod = handlers
 engine.rebuild405Handlers()
}

NoMethod 方法與 NoRoute 方法的邏輯是一致的。NoMethod 將用戶傳入的自定義 handlers 賦給 enginenoMethod 字段存儲,接下去的 engine.rebuild405Handlers():

func (engine *Engine) rebuild405Handlers() {
 engine.allNoMethod = engine.combineHandlers(engine.noMethod)
}

engine.combineHandlers(engine.noMethod)engine.noMethod 相當於我們剛剛用戶傳入的 handlers 參數,engine.combineHandlers 是一個重組 handlers 的方法:

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
 finalSize := len(group.Handlers) + len(handlers)
 if finalSize >= int(abortIndex) {
  panic("too many handlers")
 }
 mergedHandlers := make(HandlersChain, finalSize)
 copy(mergedHandlers, group.Handlers)
 copy(mergedHandlers[len(group.Handlers):], handlers)
 return mergedHandlers
}

所以 rebuild405Handlers 只是將 engine.Handlersengine.noMethod 組合起來一起賦給 allNoMethod ,這裏都不能分析出什麼,我們得進一步去看 allNoMethod 使用的地方,也就是路由進入後判斷的地方,即 ServeHTTP 中的 engine.handleHTTPRequest(c) 這一步。

handleHTTPRequest 方法的源碼:

func (engine *Engine) handleHTTPRequest(c *Context) {
 httpMethod := c.Request.Method
 rPath := c.Request.URL.Path
 unescape := false
 if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
  rPath = c.Request.URL.RawPath
  unescape = engine.UnescapePathValues
 }

 if engine.RemoveExtraSlash {
  rPath = cleanPath(rPath)
 }

 // 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 {
   // 如果 method 對應不上,則繼續下一次循環
   continue
  }
  root := t[i].root
  // 在 tree 中找到 route
  value := root.getValue(rPath, c.params, unescape)
  if value.params != nil {
   c.Params = *value.params
  }
  if value.handlers != nil {
   c.handlers = value.handlers
   c.fullPath = value.fullPath
   // 跳轉到對應的 handlers (中間件或用戶handlers)
   c.Next()
   c.writermem.WriteHeaderNow()
   return
  }
  if httpMethod != http.MethodConnect && rPath != "/" {
   if value.tsr && engine.RedirectTrailingSlash {
    redirectTrailingSlash(c)
    return
   }
   if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
    return
   }
  }
  break
 }

 // 關鍵之處,只有 HandleMethodNotAllowed 爲 true 時纔會執行
 if engine.HandleMethodNotAllowed {
  for _, tree := range engine.trees {
   if tree.method == httpMethod {
    continue
   }
   // 找不到對應的 method,405 處理
   if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
    c.handlers = engine.allNoMethod
    serveError(c, http.StatusMethodNotAllowed, default405Body)
    return
   }
  }
 }

 // 找不到路由 404 處理
 c.handlers = engine.allNoRoute
 serveError(c, http.StatusNotFound, default404Body)
}

發現在關鍵之處,是實現了 NoMethod 的邏輯處理的,只是有個前提,engine.HandleMethodNotAllowed 必須爲 true 。所以 NoMethod 並不是 TODO 狀態。

驗證

在上面的測試例子中增加一行 r.HandleMethodNotAllowed = true

......
r := gin.Default()
r.HandleMethodNotAllowed = true
......

得到了我的預期結果:

$ curl http://localhost:8080
NoMethod

$ curl -X POST http://localhost:8080
/

$ curl -X PUT http://localhost:8080
NoMethod

提 PR

既然 NoMethod 是可使用狀態,那不應該被標記爲 TODO ,而且文檔註釋中沒有提醒用戶需要將 engine.HandleMethodNotAllowed 設爲 true ,所以我嘗試將 NoMethod 方法的代碼修改爲:

// NoMethod sets the handlers called when NoMethod.
func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
 engine.HandleMethodNotAllowed = true
 engine.noMethod = handlers
 engine.rebuild405Handlers()
}

然後提交了 PR[2]

在 11 天后通過了

哈哈,容我開心一小陣 ~

PS.

不過在後面的一次 PR 中,又更改了我提交的代碼

NoMethod 中不默認開啓 engine.HandleMethodNotAllowed = true ,而是通過註釋文檔提醒用戶。

你們覺得哪種方式更好些呢?關注我加我好友或者點擊下方閱讀原文可留言哦!

參考資料

[1]

gin-gonic/gin: https://github.com/gin-gonic/gin

[2]

PR: https://github.com/gin-gonic/gin/pull/2872

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/LoY0n1BV3vomoeBEpfGX9w