Gin 源碼概覽 - 路由

本文基于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個函數,LoggerRecovery

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)

這里的代碼比較多,其實大家可以簡單的認為就是將handlepath進行判斷,注意這里的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) 在接收到請求后,最終會調用engineServeHTTP方法

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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/66368.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/66368.shtml
英文地址,請注明出處:http://en.pswp.cn/web/66368.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

docker 基礎語法學習,K8s基礎語法學習,零基礎學習

下面是關于Docker和Kubernetes的基礎語法學習資料&#xff0c;包括一些關鍵概念和示例代碼。 Docker 基礎語法 1. 安裝 Docker 首先&#xff0c;你需要安裝 Docker。以下是不同操作系統上的安裝指南&#xff1a; Windows/Mac: 下載并安裝 Docker Desktop。 Linux: 根據你的…

【逆境中綻放:萬字回顧2024我在挑戰中突破自我】

&#x1f308;個人主頁: Aileen_0v0 &#x1f525;熱門專欄: 華為鴻蒙系統學習|計算機網絡|數據結構與算法 ?&#x1f4ab;個人格言:“沒有羅馬,那就自己創造羅馬~” 文章目錄 一、引言二、個人成長與盤點情感與心理成長學習與技能提升其它榮譽 三、年度創作歷程回顧創作內容概…

職場溝通與行為

職場溝通與行為 引言 在職場上&#xff0c;你是否曾遇到過困惑的溝通&#xff1f;是否對同事的行為有過疑慮&#xff1f;這不僅是個別現象&#xff0c;而是我們這個時代工作文化中的普遍問題。許多職場的摩擦&#xff0c;來自溝通不暢或是行為不當。那么&#xff0c;如何才能…

【Linux 重裝】Ubuntu 啟動盤 U盤無法被識別,如何處理?

背景 U盤燒錄了 Ubuntu 系統作為啟動盤&#xff0c;再次插入電腦后無法被識別 解決方案&#xff08;Mac 適用&#xff09; &#xff08;1&#xff09;查找 USB&#xff0c;&#xff08;2&#xff09;格式化&#xff08;1&#xff09;在 terminal 中通過 diskutil list 查看是…

中職網絡建設與運維ansible服務

ansible服務 填寫hosts指定主機范圍和控制節點后創建一個腳本&#xff0c;可以利用簡化腳本 1. 在linux1上安裝系統自帶的ansible-core,作為ansible控制節點,linux2-linux7作為ansible的受控節點 Linux1 Linux1-7 Yum install ansible-core -y Vi /etc/ansible/hosts 添加…

數據庫服務體系結構

1. 數據庫服務應用配置 服務進行配置有什么作用&#xff1f; 實現服務運行啟動 實現某些功能 應用配置有三種方式&#xff1f; 利用編譯安裝進行配置 編寫配置文件信息 ,.默認的配置文件: /etc/my.cnf 利用啟動命令參數配置信息&#xff0c;mysqld_safe --skip-grant-tables --…

Langchain+FastApi+Vue前后端Ai對話(超詳細)

一、引入 首先可以先看下作者的文章 FastApi相關文章&#xff1a;創建最簡單FastApi的項目Vue相關文章&#xff1a;最簡單的aixos二次封裝Langchain相關文章&#xff1a;如何使用LangSmith跟蹤deepseek模型 二、后端搭建 1 項目文件結構 routers&#xff1a;存放api接口se…

如何在不暴露MinIO地址的情況下,用Spring Boot與KKFileView實現文件預覽

在現代Web應用中&#xff0c;文件預覽是一項常見且重要的功能。它允許用戶在不上傳或下載文件的情況下&#xff0c;直接在瀏覽器中查看文件內容。然而&#xff0c;直接將文件存儲服務&#xff08;如MinIO&#xff09;暴露給前端可能會帶來安全風險。本文將介紹如何在不暴露MinI…

簡歷_使用優化的Redis自增ID策略生成分布式環境下全局唯一ID,用于用戶上傳數據的命名以及多種ID的生成

系列博客目錄 文章目錄 系列博客目錄WhyRedis自增ID策略 Why 我們需要設置全局唯一ID。原因&#xff1a;當用戶搶購時&#xff0c;就會生成訂單并保存到tb_voucher_order這張表中&#xff0c;而訂單表如果使用數據庫自增ID就存在一些問題。 問題&#xff1a;id的規律性太明顯、…

Jira中bug的流轉流程

Jira中bug的狀態 1. 處理Bug的流程2. bug狀態流轉詳述bug的狀態通常包括 1. 處理Bug的流程 2. bug狀態流轉詳述 bug的狀態通常包括 未解決 1. 測試人員創建一個bug&#xff0c;填寫bug的詳細信息&#xff0c;如概要、bug級別、復現步驟、現狀、預期結果等 2. 定位bug&#x…

Linux的幾個基本指令

文章目錄 一、幾個基本指令1、ls 指令注意&#xff01; 2、pwd命令3、touch 指令4、mkdir 指令注意&#xff01;注意&#xff01; 5、cd 指令注意&#xff01; 6、cp 指令 今天我們學習Linux下的幾個基本指令&#xff0c;本篇是在Xshell環境下執行的。 一、幾個基本指令 1、…

軟件工程師歐以寧:引領無人機導航與物聯網安全的技術革新

在科技日新月異的今天,軟件工程師歐以寧憑借卓越的技術能力和前瞻性的創新思維,成為了無人機自主導航和物聯網安全領域的佼佼者。作為一名深耕技術前沿的專家,歐以寧不僅推動了無人機導航技術的突破性進展,還為智能家居和物聯網的安全架構提供了全新的解決方案。她的研究成果,以…

數據庫基礎練習1(創建表,設置外鍵,檢查,不為空,主鍵等約束)安裝mysql詳細步驟

安裝MySQL詳細步驟 1. 下載 MySQL 安裝程序 訪問 MySQL 官方網站&#xff1a;MySQL Downloads。在下載頁面&#xff0c;選擇 "MySQL Community (GPL) Downloads"。在 "MySQL Community Server" 部分&#xff0c;根據你的操作系統&#xff08;Windows&…

laravel中請求失敗重試的擴展--Guzzle

背景 開發過程中&#xff0c;跟外部接口對接時&#xff0c;很常見的要考慮到失敗重新的情況&#xff0c;這里記錄一下我用的失敗重試的情況&#xff0c; 重試方法 1、使用 Laravel 的 HTTP 客戶端和異常處理 結合異常處理和重試邏輯 use Illuminate\Support\Facades\Http;…

ThinkPHP 8的一對多關聯

【圖書介紹】《ThinkPHP 8高效構建Web應用》-CSDN博客 《2025新書 ThinkPHP 8高效構建Web應用 編程與應用開發叢書 夏磊 清華大學出版社教材書籍 9787302678236 ThinkPHP 8高效構建Web應用》【摘要 書評 試讀】- 京東圖書 使用VS Code開發ThinkPHP項目-CSDN博客 編程與應用開…

工業網口相機:如何通過調整網口參數設置,優化圖像傳輸和網絡性能,達到最大幀率

項目場景 工業相機是常用與工業視覺領域的常用專業視覺核心部件&#xff0c;擁有多種屬性&#xff0c;是機器視覺系統中的核心部件&#xff0c;具有不可替代的重要功能。 工業相機已經被廣泛應用于工業生產線在線檢測、智能交通,機器視覺,科研,軍事科學,航天航空等眾多領域 …

java使用poi-tl自定義word模板導出

文章目錄 概要整體架構流程創建word模板核心代碼導出結果 概要 在軟件開發領域&#xff0c;自定義Word模板的使用是導出格式化數據的一種常見做法。poi-tl&#xff08;Apache POI Template Language&#xff09;作為一款基于廣受認可的Apache POI庫的Word模板引擎&#xff0c;…

IDEA2023版中TODO的使用

介紹&#xff1a;TODO其實本質上還是注釋&#xff0c;只不過加上了TODO這幾個字符&#xff0c;可以讓使用者快速找到。 注意&#xff1a;在類、接口等文件中&#xff0c;注釋是使用// 即&#xff1a;// TODO 注釋內容 在配置文件中&#xff0c;注釋是使用# 即&#xff1a;# TO…

項目練習:若依管理系統字典功能-Vue前端部分

文章目錄 一、情景說明二、若依Vue相關代碼及配置1、utils代碼2、components組件3、api接口代碼4、Vuex配置5、main.js配置 三、使用方法1、html部分2、js部分 一、情景說明 我們在做web系統的時候&#xff0c;肯定會遇到一些常量選擇場景。 比如&#xff0c;性別&#xff1a;…

LabVIEW開發X光圖像的邊緣檢測

在醫療影像處理中&#xff0c;X光圖像的分析對于骨折、腫瘤等病變的檢測非常重要。X光圖像中包含許多關鍵信息&#xff0c;然而&#xff0c;由于圖像噪聲的干擾&#xff0c;直接從圖像中提取有用的特征&#xff08;如骨折的邊緣&#xff09;變得非常困難。邊緣檢測作為圖像處理…