本文基于gin 1.1 源碼解讀
https://github.com/gin-gonic/gin/archive/refs/tags/v1.1.zip
1. 注冊路由
我們先來看一段gin代碼,來看看最終得到的一顆路由樹長啥樣
func TestGinDocExp(t *testing.T) {engine := gin.Default()engine.GET("/api/user", func(context *gin.Context) {fmt.Println("api user")})engine.GET("/api/user/info/a", func(context *gin.Context) {fmt.Println("api user info a")})engine.GET("/api/user/information", func(context *gin.Context) {fmt.Println("api user information")})engine.Run()
}
看起來像是一顆前綴樹,我們后面再仔細深入源碼
1.1 gin.Default
gin.Default()
返回了一個Engine
結構體指針,同時添加了2個函數,Logger
和 Recovery
。
func Default() *Engine {engine := New()engine.Use(Logger(), Recovery())return engine
}
New
方法中初始化了 Engine
,同時還初始化了一個RouterGroup
結構體,并將Engine
賦值給RouterGroup
,相當于互相套用了
func New() *Engine {engine := &Engine{RouterGroup: RouterGroup{Handlers: nil, // 業務handle,也可以是中間件basePath: "/", // 根地址root: true, // 根路由},RedirectTrailingSlash: true,RedirectFixedPath: false,HandleMethodNotAllowed: false,ForwardedByClientIP: true,trees: make(methodTrees, 0, 9), // 路由樹}engine.RouterGroup.engine = engine// 這里使用pool來池化Context,主要目的是減少頻繁創建和銷毀對象帶來的內存分配和垃圾回收的開銷engine.pool.New = func() interface{} { return engine.allocateContext()}return engine
}
在執行完gin.Defualt后,gin的內容,里面已經默認初始化了2個handles
1.2 Engine.Get
在Get方法內部,最終都是調用到了group.handle
方法,包括其他的POST,DELETE等
- group.handle
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {// 獲取絕對路徑, 將group中的地址和當前地址進行組合absolutePath := group.calculateAbsolutePath(relativePath)// 將group中的handles(Logger和Recovery)和當前的handles合并handlers = group.combineHandlers(handlers) // 核心在這里,將handles添加到路由樹中group.engine.addRoute(httpMethod, absolutePath, handlers)return group.returnObj()
}
- group.engine.addRoute
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {// 通過遍歷trees中的內容,判斷是否在這之前,同一個http方法下已經添加過路由// trees 是一個[]methodTree 切片,有2個字段,method 表示方法,root 表示當前節點,是一個node結構體root := engine.trees.get(method) // 如果這是第一個路由則創建一個新的節點if root == nil {root = new(node)engine.trees = append(engine.trees, methodTree{method: method, root: root})}root.addRoute(path, handlers)
}
- root.addRoute(path, handlers)
這里的代碼比較多,其實大家可以簡單的認為就是將handle
和path
進行判斷,注意這里的path
不是一個完整的注冊api,而是去掉了公共前綴后的那部分字符串。
func (n *node) addRoute(path string, handlers HandlersChain) {fullPath := path // 存儲當前節點的完整路徑n.priority++ // 優先級自增1numParams := countParams(path) // 統計當前節點的動態參數個數// non-empty tree,非空路由樹if len(n.path) > 0 || len(n.children) > 0 {walk:for {// Update maxParams of the current node// 統計當前節點及子節點中最大數量的參數個數// maxParams 可以快速判斷當前節點及其子樹是否能匹配包含一定數量參數的路徑,從而加速匹配過程。(GPT)if numParams > n.maxParams {n.maxParams = numParams}// Find the longest common prefix.// This also implies that the common prefix contains no ':' or '*'// since the existing key can't contain those chars.// 計算最長公共前綴i := 0max := min(len(path), len(n.path))for i < max && path[i] == n.path[i] {i++}// Split edge,當前路徑和節點路徑只有部分重疊,且存在分歧if i < len(n.path) {// 將后綴部分創建一個新的節點,作為當前節點的子節點。child := node{path: n.path[i:],wildChild: n.wildChild,indices: n.indices,children: n.children,handlers: n.handlers,priority: n.priority - 1,}// Update maxParams (max of all children)// 路徑分裂時,確保當前節點及子節點中是有最大的maxParamsfor i := range child.children {if child.children[i].maxParams > child.maxParams {child.maxParams = child.children[i].maxParams}}n.children = []*node{&child}// []byte for proper unicode char conversion, see #65n.indices = string([]byte{n.path[i]})n.path = path[:i]n.handlers = niln.wildChild = false}// Make new node a child of this nodeif i < len(path) {path = path[i:] // 獲取當前節點中非公共前綴的部分// 當前節點是一個動態路徑// TODO 還需要好好研究一下if n.wildChild {n = n.children[0]n.priority++// Update maxParams of the child nodeif numParams > n.maxParams {n.maxParams = numParams}numParams--// Check if the wildcard matches// 確保新路徑的動態部分與已有動態路徑不會沖突。if len(path) >= len(n.path) && n.path == path[:len(n.path)] {// check for longer wildcard, e.g. :name and :namesif len(n.path) >= len(path) || path[len(n.path)] == '/' {continue walk}}panic("path segment '" + path +"' conflicts with existing wildcard '" + n.path +"' in path '" + fullPath + "'")}// 獲取非公共前綴部分的第一個字符c := path[0]// slash after param// 當前節點是動態參數節點,且最后一個字符時/,同時當前節點還只有一個字節if n.nType == param && c == '/' && len(n.children) == 1 {n = n.children[0]n.priority++continue walk}// Check if a child with the next path byte existsfor i := 0; i < len(n.indices); i++ {if c == n.indices[i] {i = n.incrementChildPrio(i)n = n.children[i]continue walk}}// Otherwise insert itif c != ':' && c != '*' {// []byte for proper unicode char conversion, see #65n.indices += string([]byte{c})child := &node{maxParams: numParams,}n.children = append(n.children, child)// 增加當前子節點的優先級n.incrementChildPrio(len(n.indices) - 1)n = child}n.insertChild(numParams, path, fullPath, handlers)return} else if i == len(path) { // Make node a (in-path) leafif n.handlers != nil {panic("handlers are already registered for path ''" + fullPath + "'")}n.handlers = handlers}return}} else { // Empty treen.insertChild(numParams, path, fullPath, handlers)n.nType = root}
}
1.3 Engine.Run
func (engine *Engine) Run(addr ...string) (err error) {defer func() { debugPrintError(err) }()// 處理一下web的監聽地址address := resolveAddress(addr)debugPrint("Listening and serving HTTP on %s\n", address)// 最終還是使用http來啟動了一個web服務err = http.ListenAndServe(address, engine)return
}
2. 路由查找
在上一篇文章中介紹了,http 的web部分的實現,http.ListenAndServe(address, engine)
在接收到請求后,最終會調用engine
的ServeHTTP
方法
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {// 從context池中獲取一個Contextc := engine.pool.Get().(*Context) // 對Context進行一些初始值操作,比如賦值w和reqc.writermem.reset(w)c.Request = reqc.reset()// 最終進入這個方法來處理請求engine.handleHTTPRequest(c)// 處理結束后將Conetxt放回池中,供下一次使用engine.pool.Put(c)
}
2.1 engine.handleHTTPRequest()
func (engine *Engine) handleHTTPRequest(context *Context) {httpMethod := context.Request.Method // 當前客戶端請求的http 方法path := context.Request.URL.Path // 查詢客戶端請求的完整請求地址// Find root of the tree for the given HTTP methodt := engine.treesfor i, tl := 0, len(t); i < tl; i++ {if t[i].method == httpMethod {root := t[i].root// Find route in treehandlers, params, tsr := root.getValue(path, context.Params)if handlers != nil {context.handlers = handlers // 所有的handles 請求對象context.Params = params // 路徑參數,例如/api/user/:id , 此時id就是一個路徑參數context.Next() // 執行所有的handles方法context.writermem.WriteHeaderNow()return}}}// 這里客戶端請求的地址沒有匹配上,同時檢測請求的方法有沒有注冊,若沒有注冊過則提供請求方法錯誤if engine.HandleMethodNotAllowed {for _, tree := range engine.trees {if tree.method != httpMethod {if handlers, _, _ := tree.root.getValue(path, nil); handlers != nil {context.handlers = engine.allNoMethodserveError(context, 405, default405Body)return}}}}// 路由地址沒有找到context.handlers = engine.allNoRouteserveError(context, 404, default404Body)
}
2.2 context.Next()
這個方法在注冊中間件的使用會使用的較為頻繁
func (c *Context) Next() {// 初始化的時候index 是 -1c.index++s := int8(len(c.handlers))for ; c.index < s; c.index++ {c.handlers[c.index](c) // 依次執行注冊的handles}
}
我們來看一段gin 是執行中間件的流程
func TestGinMdls(t *testing.T) {engine := gin.Default()engine.Use(func(ctx *gin.Context) {fmt.Println("請求過來了")// 這里可以做一些橫向操作,比如處理用戶身份,cors等ctx.Next()fmt.Println("返回響應")})engine.GET("/index", func(context *gin.Context) {fmt.Println("index")})engine.Run()
}
curl http://127.0.0.1:8080/index
通過響應結果我們可以分析出,請求過來時,先執行了Use中注冊的中間件,然后用戶調用ctx.Next() 可以執行下一個handle,也就是用戶注冊的/index方法的handle