这篇文章记录一次给 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
赋给 engine
的 noMethod
字段存储,接下去的 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.Handlers
和 engine.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
既然 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 天后通过了
哈哈,容我开心一小阵 ~
不过在后面的一次 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