在接受requst的過程中,核心是router,目的是為了找到path對應的處理函數handler。router實現在Multiplexer(復用器)中,golang中Multiplexer的實現基于 ServeMux 結構。

三、 net/http 庫

注:本文基于go1.16版本分析,注意不同版本存在的差異

流程圖

一種簡單的實現方式

main函數中間兩行代碼分別實現了路由注冊 和 啟動服務 功能。其中 啟動服務http.ListenAndServe 的實現如下:首先創建一個Server實例 server,然后調用server的同名方法ListenAndServe。代碼如下

// http.ListenAndServe
func ListenAndServe(addr string, handler Handler) error {
server := Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}

type Server struct {
Addr string
Handler Handler
...
}

type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

在Server結構體中最關鍵的字段 Handler 是一個interface。任何結構體,只要實現了ServeHTTP方法,就可以稱之為Handler對象。通過后面流程我們可以知道,處理client請求的協程正是調用了Server.Handler的ServeHTTP方法去處理業務邏輯。因此一種最簡單的實現http請求調用的方式是,我們將業務處理函數包裝成Handler對象直接通過http.ListenAndServe方法的第二個參數傳入,代碼如下


func main() {
http.ListenAndServe("127.0.0.0:8000", Hello{})
}

type Hello struct{}

func (*Hello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Println("hello world")
}

Multiplexer和路由注冊

上面這種實現方法有個顯而易見的問題:所有請求都執行同一個handler,沒有辦法根據不同path去進行不同處理。但http框架的核心功能就是能進行 路由注冊 和 路由查找, ServeMux 結構體正是用來解決這個痛點的。我們關注到ServeMux有一個結構為map的m字段,m的key為url,value為muxEntry結構,而在muxEntry中定義存儲了具體的url和handler函數。所有跟業務相關的path和handler映射信息都是通過m存在ServeMux中。

type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // slice of entries sorted from longest to shortest.
hosts bool // whether any patterns contain hostnames
}

type muxEntry struct {
h Handler
pattern string
}

現在看一下http.HandleFunc方法如何實現注冊路由。這里引入ServeMux的實例DefaultServeMux對象(在后面的路由查找中也會用到它),從流程圖不難看出http.HandleFunc方法通過它調用ServeMux的同名方法HandleFunc,內部調用ServeMux.Handle方法,完成實際的路由注冊功能,代碼如下

// http.HandleFunc
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}

func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
...
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
...
}

那么在ServeMux.HandleFunc中為啥要將傳入的handler封裝成HandlerFunc呢?在前文我們知道處理client請求的協程通過Server.Handler處理業務邏輯,而為了能在路由查找之后直接調用該handler,net包使用了適配器模式 對帶有func(ResponseWriter, *Request)簽名的處理函數進行統一封裝。從下面的代碼可以看到,HandlerFunc實現了Handler interface,它的ServeHTTP方法正是執行HandlerFunc方法本身。

type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}

路由查找和處理請求

注冊好路由之后,我們該如何在第二步啟動服務中實現路由查找呢?net包中的做法是讓ServerMux實現Handler interface,在ServerMux.ServerHttp中封裝路由查找和處理函數的執行。這么做是采用了裝飾模式 ,能將ServerMux作為參數傳入Server.Handler中,從而實現更簡潔地調用。從流程圖我們可以看到,在http.ListenAndServe方法中如果不傳入handler,后面Server對象會使用DefaultServeMux作為默認的Handler,從而通過它調用ServerMux.ServerHttp方法實現路由查找并執行。代碼如下:

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
...
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
...
return mux.handler(host, r.URL.Path)
}

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
...
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
...
}

func (mux *ServeMux) match(path string) (h Handler, pattern string)
v, ok := mux.m[path]
if ok {
return v.h, v.pattern
}
for _, e := range mux.es {
if strings.HasPrefix(path, e.pattern) {
return e.h, e.pattern
}
}
return nil, ""
}

讓我們看一下在第二步啟動服務中都做了些什么?http.ListenAndServe方法創建了一個Server對象,并且調用Server.ListenAndServe。該方法會初始化監聽地址Addr,同時調用Listen方法設置監聽。最后將監聽的TCP對象傳入Server.Serve方法。

func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}

Serve方法主要職能是創建一個上下文對象,然后調用Listen的Accept方法用來獲取連接數據并用newConn方法創建連接對象。最后起個goroutine處理連接請求。因為每個連接都起了一個協程,請求的上下文不同,同時又保證了go的高并發。

func (srv *Server) Serve(l net.Listener) error {
...
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, err := l.Accept()
...
c := srv.newConn(rw)
c.setState(c.rwc, StateNew, runHooks)
go c.serve(connCtx)
}
}

newConn創建的實例調用自己的serve方法,完成后面的邏輯處理。serve方法使用defer定義了函數退出時連接關閉的相關處理,然后讀取連接的數據并處理讀取完畢時的狀態,其中核心部分是接下來調用的 serverHandler{c.server}.ServeHTTP(w, w.req) 處理請求。(方法太長,這里就不放源碼了) serverHandler只有Server結構一個字段,方法中傳入的是在服務啟動時創建的Server實例。它調用自己的ServeHTTP方法,并在該接口方法中做了一個重要的事情:初始化Handler。如果server對象沒有指定Handler,則使用DefaultServeMux作為默認值,并調用該Handler的ServeHTTP方法執行業務邏輯。就像在前文說到的,net包正是將路由查找和業務函數執行封裝在ServeMux.ServeHTTP方法中,讓我們可以通過不指定Handler的方式直接使用它。至此一個client請求的處理就已經完成了。

type serverHandler struct {
srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
...
handler.ServeHTTP(rw, req)
}

總結

net包通過http.HandlerFunc將路由和處理函數進行綁定,添加到DefaultServeMux的m字段里;在http.ListenAndServe方法中默認通過DefaultServeMux對象調用ServeMux.ServeHTTP來實現路由查找并執行handler。同時net包也暴露給開發者Handler interface這個http服務的核心入口,讓開發者可以定義一個結構體實現interface來替換掉DefaultServeMux,這樣就能在自定義的結構體中實現更加豐富的功能。開發者需要做的僅僅是在在服務啟動的時候將自定義結構體作為handler參數傳入。接下來要介紹的gin框架正是這么實現的。

四、gin框架

雖然我們可以通過net/http實現一個簡單的具備路由功能的http服務,但是net/http本身提供的功能比較簡單,不支持用戶以中間件的形式自定義能力。而且該包暴露的函數簽名參數是(w http.ResponseWriter, req *http.Request),開發者解析請求和回寫結果都不是很方便,因此產生了很多優秀的http框架。其中就有我們的主角gin框架,gin框架具體有以下特點:

流程圖

圖中藍色部分為net/http內部邏輯,綠色部分為gin框架實現邏輯,通過這個圖可以很好地理解net/http和gin框架的邊界,清楚gin框架在處理流程中所做的事情。從gin框架圖我們發現實際上框架核心邏輯就是實現一個以Engine結構為核心的路由,用它代替了 net/http 包的 DefaultServeMux并實現其邏輯。整個路由注冊和請求處理的流程在上文中有比較清晰的闡述,所以接下來的重點放在理解gin框架的核心Engine結構,其中包括context、router tree、RouterGroup、中間件與請求鏈條等部分。在接下來的介紹中,我們能夠對一個成熟的http服務框架所具備的能力以及它們的實現有個比較清晰的認知,對之后搭建自己的http服務框架有個好的參考。

Engine

Engine是gin框架框架的入口,是框架的核心發動機。我們通過Engine對象來進行服務路由注冊和查找、組裝業務處理函數和中間件、進行路由組的管理。這幾部分的能力都是通過其核心字段trees和RouteGroup實現的。

// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery()) // 默認加載日志和異常處理兩個中間件
return engine
}

// New returns a new blank Engine instance without any middleware attached.
func New() *Engine {
debugPrintWARNINGNew()
// 嵌套RouterGroup,實現路由相關的注冊
engine := Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
...
// 路由樹,根據路徑快速查找handlers,對于九種http請求方法分別生成單獨的路由樹
trees: make(methodTrees, 0, 9),
...
}
// RouterGroup嵌套Engine結構,能調用engine的方法
engine.RouterGroup.engine = engine
// 通過對象池管理Context,減輕GC壓力,提升系統性能
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
return engine
}

// 添加路由
func (engine *Engine) addRoute(method, path string, handlers HandlersChain)

Engine對象包含一個addRoute方法用于添加URL請求處理器,它會將請求對應的路徑和處理器掛接到相應的請求樹中。Engine通過RouterTree進行路由的注冊和查找。

RouterTree

在gin框架中,Engine.trees 使用是基于字典樹的httprouter,路由查找效率高且節省存儲空間。路由規則被分成了最多9棵前綴樹,每一個HTTP Method(POST/GET/…)對應一棵前綴樹,樹的節點按照URL中的/符號進行層級劃分,URL支持 :name 形式的名字匹配。

之所以這么設計,在httprouter的README.md中是這么描述的:由于 URL 路徑具有分層結構并且僅使用有限的一組字符,因此會存在許多公共前綴。這使我們能夠輕松地將路由拆分為更小的部分。此外路由器為每個請求方法管理一個單獨的樹,一方面它比在每個節點中保存一個方法去映射更節省空間;另一方面它還允許我們在開始查找前綴樹之前減少很多路由問題。

type methodTree struct {
method string
root *node
}

type node struct {
path string
indices string
wildChild bool
nType nodeType
priority uint32
children []*node // child nodes, at most 1 :param style node at the end of the array
handlers HandlersChain
fullPath string
}

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc

每個節點除了保存按/符號分割的URL的某段path之外,還會掛接若干請求處理函數構成一個請求處理鏈 HandlersChain。每當對請求進行路由查找時,在這棵樹上找到的請求URL對應的節點,拿到對應的請求處理鏈進行組裝,等待之后的執行。

RouterGroup

我們經常會遇到類似的場景,需要基于版本或者模塊將相同前綴的路由放在一起,方便使用。而Engine對象通過RouterGroup對路由實現了分組管理,并且支持分組嵌套和對組設置中間件。RouterGroup是對路由樹的包裝,所有的路由規則最終都是由它來進行管理的。Engine結構體繼承了RouterGroup,所以Engine直接具備了RouterGroup所有的路由管理功能。同時RouterGroup對象里還會包含一個Engine的指針,可以調用engine的addRoute方法。

type Engine struct {
RouterGroup
...
}

type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}

type IRouter interface {
Use(...HandlerFunc) IRoutes // 注冊中間件

// Handle、Any等方法調用handle完成路由注冊
Handle(string, string, ...HandlerFunc) IRoutes
Any(string, ...HandlerFunc) IRoutes
...

Group(string, ...HandlerFunc) *RouterGroup // 創建一個新的路由組
}

// 路由注冊的入口方法
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 (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
return joinPaths(group.basePath, relativePath)
}

// 合并處理函數
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
...
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}

RouterGroup實現了IRouter接口,暴露了一系列路由方法,這些方法最終都是通過調用Engine.addRoute方法將請求處理器掛接到路由樹中。RouterGroup內部有一個前綴路徑字段basePath,它會調用calculateAbsolutePath方法將所有的子路徑都加上這個前綴再放進路由樹中。有了這個前綴路徑,就可以實現URL分組功能。RouterGroup調用combineHandlers方法將分組嵌套下所有組維度設置的中間件和請求處理函數進行組裝成handlers。

中間件與請求鏈

在gin框架中插件和業務處理函數形式是一樣的,都是func(*Context),函數鏈前面的是插件函數,業務處理函數在鏈的最尾端。當我們定義路由時,gin框架會將插件函數和業務處理函數合并在一起形成鏈條結構HandlersChain。

type Context struct {
...
handlers HandlersChain
index int8
...
}

// 挨個調用函數鏈中的處理函數
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}

const abortIndex int8 = math.MaxInt8 >> 1

func (c *Context) Abort() {
c.index = abortIndex
}

gin框架在接收到客戶端請求后,通過路由樹找到相應的處理鏈,構造一個Context對象,再調用它的Next()方法進入請求處理流程。

gin支持Abort()方法中斷請求鏈的執行,它的原理是將Context.index調整到一個比較大的數字,這樣Next()方法中的調用循環就會立即結束。因為執行Abort()方法之后,需要讓當前函數內后面的代碼邏輯繼續執行,所以不能通過panic的方式或者事件等方式中斷執行流。如果在插件中顯示調用Next()方法,那么它就改變了正常的執行順序執行流,會嵌套執行流,嵌套執行流是讓后續的處理器在前一個處理器進行到一半的時候執行,等后續處理器完成執行后,再回到前一個處理器繼續往下執行。

Context

gin框架會為每個請求分配單獨的Context,其中包含了請求的參數、響應、engine、handlers等全部上下文信息,并且Context會貫穿這次請求的所有流程。由于分配給每個請求Context,當百萬并發到來時,頻繁的創建對象會給golang的GC帶來非常大的壓力,因此gin框架就利用sync.Pool將Context對象復用起來。

type Context struct {
writermem responseWriter
Request *http.Request // 請求對象
Writer ResponseWriter // 響應對象

Params Params // URL路徑匹配參數
handlers HandlersChain // 需要處理的請求鏈
index int8
fullPath string

engine *Engine
params *Params
skippedNodes *[]skippedNode
mu sync.RWMutex

Keys map[string]interface{} // 自定義上下文信息
Errors errorMsgs // 函數鏈記錄的每個handler的錯誤信息
Accepted []string

queryCache url.Values
formCache url.Values
sameSite http.SameSite
}

Context對象提供了非常豐富的方法用于獲取當前請求的上下文信息,提供了很多內置的數據綁定和響應形式,其中包括JSON、HTML、Protobuf、MsgPack、Yaml等,它會為每一種形式都單獨定制一個渲染器。所有的渲染器最終調用內置的http.ResponseWriter(Context.Writer)將響應對象轉換成字節流寫到socket中。

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()

engine.handleHTTPRequest(c)

engine.pool.Put(c)
}

Engine.ServeHTTP方法中,每次響應請求都會先從臨時對象池中取一個context對象,使用完之后再放回取。需要注意這個context是從臨時對象池中取出后再reset,而不是使用完之后reset。所以這個context可能會包含上一次請求的上下文信息,如果上一次請求開啟新的協程使用context,那么新請求會reset這個context。如果需要在新協程里保留上下文信息,可以通過Context.Copy() copy這個context進行參數傳遞 。

文章轉自微信公眾號@IEG增長中臺技術團隊

上一篇:

Go工程化(五) API 設計下: 基于 protobuf 自動生成 gin 代碼

下一篇:

Gin系列二:Gin搭建Blog API's (一)
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費