一、高性能
-
使用sync.pool解決頻繁創建的context對象,在百萬并發的場景下能大大提供訪問性能和減少GC
// ServeHTTP conforms to the http.Handler interface. // 每次的http請求都會從sync.pool中獲取context,用完之后歸還到pool中 func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {c := engine.pool.Get().(*Context)c.writermem.reset(w) //重置 responsewriterc.Request = reqc.reset() //重置使用過的context各個屬性engine.handleHTTPRequest(c)engine.pool.Put(c) }// 這里給pool指定了一個創建新對象的函數,注意不是所有的請求共用一個context,context在高并發場景下可能會分配多個,但是遠遠小于并發協程數量。 sync.pool.new是可能并發調用的,所以內部的邏輯需要保障線程安全 func New(opts ...OptionFunc) *Engine {... engine.RouterGroup.engine = engineengine.pool.New = func() any {return engine.allocateContext(engine.maxParams)}return engine.With(opts...) }//每次http請求都需要分配一個context,這個初始context初始化了兩個數組的最大容量 func (engine *Engine) allocateContext(maxParams uint16) *Context {v := make(Params, 0, maxParams)skippedNodes := make([]skippedNode, 0, engine.maxSections)return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes} }
-
前綴樹路由(類似httprouter的路由,提升性能近40倍)
gin在v1.0版本開始放棄了
github.com/julienschmidt/httprouter
,重新實現了一套路由;對比gin新實現的路由基本上采用了httprouter的邏輯,但是和框架結合的更加完整,比如說把httprouter中router的能力提到了engine中。 -
json序列化優化
gin提供了四種可選的json序列化方式,默認情況下會使用encoding/json
/github.com/gin-gonic/gin@v1.10.0/internal/json-- go_json.go ("github.com/goccy/go-json")-- json.go ("encoding/json")-- jsoniter.go ("github.com/json-iterator/go")-- sonic.go ("github.com/bytedance/sonic")
需要在編譯期間指定tag來決定使用哪種序列化工具
go run -tags={go_json|jsoniter|sonic|(不指定默認encoding)} main.go
通過一個簡單的基準測試看看哪種json序列化效率更高
package ganimport ("encoding/json"sonicjson "github.com/bytedance/sonic"gojson "github.com/goccy/go-json"itjson "github.com/json-iterator/go""testing" )func BenchmarkJson(b *testing.B) {jsonStr := `{"name":"zhangsan","age":18,"address":"beijing"}`m := &map[string]interface{}{}for i := 0; i < b.N; i++ {json.Unmarshal([]byte(jsonStr), m)json.Marshal(m)} }func BenchmarkGOJson(b *testing.B) {jsonStr := `{"name":"zhangsan","age":18,"address":"beijing"}`m := &map[string]interface{}{}for i := 0; i < b.N; i++ {gojson.Unmarshal([]byte(jsonStr), m)gojson.Marshal(m)} }func BenchmarkItJson(b *testing.B) {m := &map[string]interface{}{}jsonStr := `{"name":"zhangsan","age":18,"address":"beijing"}`for i := 0; i < b.N; i++ {itjson.Unmarshal([]byte(jsonStr), m)itjson.Marshal(m)} }func BenchmarkSonicJson(b *testing.B) {m := &map[string]interface{}{}jsonStr := `{"name":"zhangsan","age":18,"address":"beijing"}`for i := 0; i < b.N; i++ {sonicjson.Unmarshal([]byte(jsonStr), m)sonicjson.Marshal(m)} }
測試結果: sonic > go_json > json_iterator > encoding
$ go test -bench='Json$' -benchtime=5s -benchmem . goos: windows goarch: amd64 pkg: gan cpu: Intel(R) Core(TM) i5-10400 CPU @ 2.90GHz BenchmarkJson-12 2632854 2260 ns/op 616 B/op 24 allocs/op BenchmarkGOJson-12 5226374 1142 ns/op 248 B/op 11 allocs/op BenchmarkItJson-12 4811112 1260 ns/op 400 B/op 19 allocs/op BenchmarkSonicJson-12 6616218 913.0 ns/op 333 B/op 10 allocs/op PASS ok gan 30.313s
二、基于前綴樹的路由設計
-
go語言中原生net/http包在負載路由下的缺陷
動態路由:缺少例如hello/:name,hello/*這類的規則。
鑒權:沒有分組/統一鑒權的能力,需要在每個路由映射的handler中實現。 -
http請求怎么進入的gin的處理邏輯中去的?
-
gin框架中調用了net/http包,監聽address,請求都會進入頂層路由,也就是engine結構實現的ServeHTTP函數中
func (engine *Engine) Run(addr ...string) (err error) {......debugPrint("Listening and serving HTTP on %s\n", address)err = http.ListenAndServe(address, engine.Handler())return } func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {......engine.handleHTTPRequest(c)...... }
-
在engine.handleHTTPRequest? 中,根據request中的path,在路由表中獲取路由對應的handler然執行
func (engine *Engine) handleHTTPRequest(c *Context) {httpMethod := c.Request.MethodrPath := c.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 {continue}root := t[i].root// Find route in treevalue := root.getValue(rPath, c.params, c.skippedNodes, unescape)if value.params != nil {c.Params = *value.params}if value.handlers != nil {c.handlers = value.handlersc.fullPath = value.fullPathc.Next() //最終執行的handlerc.writermem.WriteHeaderNow()return}......} }
-
-
路由的注冊和匹配
-
Engine繼承了RouterGroup的能力,當我們注冊handler時,或者在分組路由下去注冊handler時,其實都是調用的RouterGroup的能力
type Engine struct {RouterGroup...... }
-
RouterGroup中保留了一個Engine指針,最終的路由是注冊到了Engine中的路由表中,這個路由表沒有設計在RouterGroup中,保證了RouterGroup的職責單一性,也就是只做路由分組。
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {......group.engine.addRoute(httpMethod, absolutePath, handlers)return group.returnObj() }
-
Engine中保存了一個methodTree對象,這個就是路由表,是一個樹狀結構
type methodTree struct {method stringroot *node }type node struct {path stringindices stringwildChild boolnType nodeTypepriority uint32children []*node // child nodes, at most 1 :param style node at the end of the arrayhandlers HandlersChainfullPath string }type methodTrees []methodTree
-
三、中間件執行流程
-
中間件執行鏈路
-
中間件保存在GroupRouter中
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {group.Handlers = append(group.Handlers, middleware...)return group.returnObj() }
-
每聲明一次分組,都會將handler進行一次合并
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {return &RouterGroup{Handlers: group.combineHandlers(handlers), //合并中間處理鏈路basePath: group.calculateAbsolutePath(relativePath),engine: group.engine,} }
-
以上只是將中間件合并成了一條鏈路,最終的業務邏輯會在執行GET.POST等請求方法時,拼接到執行鏈路末端
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {absolutePath := group.calculateAbsolutePath(relativePath)handlers = group.combineHandlers(handlers) //這里參數的handlers是業務邏輯,合并到鏈路末尾。group.engine.addRoute(httpMethod, absolutePath, handlers)return group.returnObj() }
-
最終效果
-
-
前置處理和后置處理
-
假設需要對一段業務邏輯采集它的執行耗時,一般我們需要在執行邏輯前聲明一個時間點,執行完后再用當前時間減去執行前的時間得到耗時。
-
在gin中的實現方式是通過context的next方法去實現的,結合上面的流程,在中間件中每聲明一次next,執行鏈會先去執行下一個handler,最終的效果應該是ABBA方式的執行。
func (c *Context) Next() {c.index++for c.index < int8(len(c.handlers)) {c.handlers[c.index](c)c.index++} }
-
其他:
Gin的核心特性
- 高性能:Gin的HTTP請求處理速度極快,能夠支持大量的并發連接(gin并不是基準測試中最快的框架,但是相對較快,簡單好用,用戶體量大)
- 簡單易用:Gin的API設計直觀,使得開發者可以快速上手并構建應用。
- 中間件支持:Gin允許開發者通過中間件來擴展其功能,這為構建復雜的Web應用提供了可能。
- 路由和參數綁定:Gin提供了強大的路由功能,支持參數綁定,使得URL處理更加靈活。
- 數據渲染:Gin支持多種數據渲染方式,包括JSON、XML、HTML等,方便開發者根據需求選擇合適的輸出格式。