golang gin大行其道!
前言
很多人已經(jīng)在api接口項目這塊,已經(jīng)用上了gin,我自己也用上了,感覺挺好用的,就寫了這篇文章來分享下我對gin的理解和拾遺。gin對于我的理解還不能算是一個api框架,因為它是基于net/http包來處理http請求處理的,包括監(jiān)聽連接,解析請求,處理響應都是net/http包完成的,gin主要是做了路由前綴樹的構(gòu)建,包裝http.request,http.response,還有一些快捷輸出響應內(nèi)容格式的工具功能(json.Render,xml.Render,text.Render,proto.Render)等,接下來主要從兩個方面去分析gin。
gin構(gòu)建路由
gin處理請求
一,gin構(gòu)建路由
下面是gin用來構(gòu)建路由用到的結(jié)構(gòu)代碼type Engine struct {RouterGrouptrees methodTrees省略代碼}// 每個http.method (GET,POST,HEAD,PUT等9種)都會構(gòu)建成單獨的前綴樹type methodTree struct {// 方法: GET,POST等method string// 對應方法的前綴樹的根節(jié)點root *node}//GET,POST前綴樹等構(gòu)成的切片type methodTrees []methodTreetype node struct {// 節(jié)點的當前路徑path string// 子節(jié)點的首字母構(gòu)成的字符串,數(shù)量和位置對應children切片indices string// 子節(jié)點children []*node// 當前匹配到的路由,處理的handlerhandlers HandlersChain// 統(tǒng)計子節(jié)點優(yōu)先級priority uint32// 節(jié)點類型nType nodeTypemaxParams uint8wildChild bool// 當前節(jié)點的全路徑,從root到當前節(jié)點的全路徑fullPath string}
用戶定義路由router := gin.New()router.GET("banner/detail", PkgHandler)router.PrintTrees()router.GET("/game/detail", PkgHandler)router.PrintTrees()router.GET("/geme/detail", PkgHandler)router.GET 調(diào)用鏈這里提一下HandlerFunc,就是gin常說的中間件,是一個函數(shù)類型,只要實現(xiàn)該函數(shù)類型就可以用作中間件來處理邏輯func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {return group.handle(http.MethodGet, relativePath, handlers)}func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {absolutePath := group.calculateAbsolutePath(relativePath)handlers = group.combineHandlers(handlers)group.engine.addRoute(httpMethod, absolutePath, handlers)return group.returnObj()}func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {assert1(path[0] == '/', "path must begin with '/'")assert1(method != "", "HTTP method can not be empty")assert1(len(handlers) > 0, "there must be at least one handler")debugPrintRoute(method, path, handlers)// 根據(jù)method取出當前方法前綴樹的rootroot := engine.trees.get(method)if root == nil {root = new(node)root.fullPath = "/"engine.trees = append(engine.trees, methodTree{method: method, root: root})}// 這里就是構(gòu)建路由前綴樹的邏輯(避免閱讀不友好,這里就不繼續(xù)貼下去了)root.addRoute(path, handlers)}
?/
banner/detail(路由最前面如果沒有/開頭的話,gin會自動補上/)/game/detail/geme/detail?
上面三個路由地址,構(gòu)建路由前綴樹的過程(這里不討論:和 *)

最后的路由樹結(jié)構(gòu)就是上面這個樣子?
gin的RouterGroup路由組的概念(group和use方法),主要是給后面添加路由用來傳遞path和handlers和繼承之前path和handlers,這里不細講
二,gin處理請求
先簡述下net/http包處理的整個流程1. 解析數(shù)據(jù)包,解析成http請求協(xié)議,存在http.request, 包裝請求頭,請求體,當前連接conn【read(conn) 讀取數(shù)據(jù)】2. 外部實現(xiàn)handler接口,調(diào)用外部邏輯處理請求(這里外部就是gin)3. 根據(jù)用戶邏輯,返回http響應給客戶端 存在http.response,包裝響應頭,響應體,當前連接conn【write(conn) 返回數(shù)據(jù)】這是net/http包提供給外部,用于處理http請求的handler接口,只要外部實現(xiàn)了此接口,并且傳遞給http.server.Handler,就可以執(zhí)行用戶handler邏輯,gin的Engine實現(xiàn)了Handler 接口,所以gin就介入了整個http請求流程中的用戶邏輯那部分。即上面的第二點。type Handler interface {ServeHTTP(ResponseWriter, *Request)}func ListenAndServe(addr string, handler Handler) error {server := &Server{Addr: addr, Handler: handler}return server.ListenAndServe()}serverHandler{c.server}.ServeHTTP(w, w.req)func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {handler := sh.srv.Handlerif handler == nil {handler = DefaultServeMux}if req.RequestURI == "*" && req.Method == "OPTIONS" {handler = globalOptionsHandler{}}//在這里最終會調(diào)用用戶傳進來的handler接口的實現(xiàn),并且把rw = http.response,req = http.request傳遞到gin處理邏輯里面去,這樣gin既能讀取http請求內(nèi)容,也能操作gin輸出到客戶端handler.ServeHTTP(rw, req)}// ServeHTTP conforms to the http.Handler interface.// gin.Engine實現(xiàn)了http.Handler接口,可以處理請求func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {c := engine.pool.Get().(*Context)c.writermem.reset(w)c.Request = reqc.reset()engine.handleHTTPRequest(c)//請求完成,放回池中engine.pool.Put(c)}
?
所以,gin處理請求的入口ServeHTTP這里
1. 每個請求都會用到單獨gin.context,包裝http.response, http.request,通過在中間件中傳遞gin.context,從而用戶可以拿到請求相關信息,根據(jù)自己邏輯可以處理請求和響應,context也用了pool池化,減少內(nèi)存分配
2. 根據(jù)上面?zhèn)鬟f的請求路徑,和method,在對應方法的路由前綴樹上查詢到節(jié)點,即就找到要執(zhí)行的handlers,就執(zhí)行用戶定義的中間件邏輯(前綴樹的查詢也跟構(gòu)建流程類似,可以參考上面的圖)
func (engine *Engine) handleHTTPRequest(c *Context) {httpMethod := c.Request.MethodrPath := c.Request.URL.Pathunescape := falseif engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {rPath = c.Request.URL.RawPathunescape = engine.UnescapePathValues}if engine.RemoveExtraSlash {rPath = cleanPath(rPath)}fmt.Printf("httpMethod: %s, rPath: %s\n", httpMethod, rPath)// 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 {continue}root := t[i].root// Find route in tree// 根據(jù)請求路徑獲取對應方法樹上的節(jié)點value := root.getValue(rPath, c.Params, unescape)if value.handlers != nil {// 獲取對應節(jié)點上的中間件(即handler)c.handlers = value.handlersfmt.Printf("c.handlers: %d\n", len(c.handlers))c.Params = value.paramsc.fullPath = value.fullPathc.Next()c.writermem.WriteHeaderNow()return}省略代碼}省略代碼}handlers有2種方式1. 在handler中不手動調(diào)用c.next的話,類似于隊列,先進先執(zhí)行2. 如果手動調(diào)用c.next的話,類似于洋蔥模型,包裹剝開概念的執(zhí)行func (c *Context) Next() {//c.index從-1開始c.index++for c.index < int8(len(c.handlers)) {c.handlers[c.index](c)c.index++}}中斷整個調(diào)用鏈,從當前函數(shù)返回func (c *Context) Abort() {//直接將中間件索引改成最大限制的值,從而退出for循環(huán)c.index = abortIndex}
貼下圖,方便理解這個中間件是怎么依次執(zhí)行的

gin執(zhí)行完handlers之后,就回歸到net/http包里面,就會finishRequest()
serverHandler{c.server}.ServeHTTP(w, w.req)w.cancelCtx()if c.hijacked() {return}flush數(shù)據(jù), write到connw.finishRequest()
總結(jié)
1. gin存儲路由結(jié)構(gòu),用到了類似前綴樹的數(shù)據(jù)結(jié)構(gòu),在存儲方面可以共用前綴節(jié)省空間,但是查找方面應該不是很優(yōu)秀,為什么不用普通的hash結(jié)構(gòu),key-value方式,存儲路由和handlers,因為畢竟一個項目里面,定義的路由路徑應該不會很多,使用hash存儲應該不會占用很多內(nèi)存。
但是如果每次請求都要去查找一下路由前綴樹的話會比hash結(jié)構(gòu)慢很多(請求量越大應該越明顯)。
2. gin是基于net/http官方包來處理http請求整個流程
3. gin的中間件流程很好用
推薦閱讀
站長 polarisxu
自己的原創(chuàng)文章
不限于 Go 技術(shù)
職場和創(chuàng)業(yè)經(jīng)驗
Go語言中文網(wǎng)
每天為你
分享 Go 知識
Go愛好者值得關注
