1. Gin簡介
前面透過兩篇文章分享了Golang HTTP程式設計的路由分發、請求/響應處理。
可以看出來Golang原生HTTP程式設計在路由分組、動態路由及引數讀取/驗證、構造String/Data/JSON/HTML響應的方法等存在最佳化的空間。
Gin是一個用Golang編寫的高效能Web框架。
- 基於字首樹的路由,快速且支援動態路由
- 支援中介軟體及路由分組,將具有同一特性的路由劃入統一組別、設定相同的中介軟體。
- 比如需要登入的一批介面接入登入許可權認證中介軟體、而不需要登入一批介面則不需要接入
- ...
2. 快速使用
基於gin@v1.8.1
,基本使用如下
func main() {
// Creates a new blank Engine instance without any middleware attached
engine := gin.New()
// Global middleware
// Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
// By default gin.DefaultWriter = os.Stdout
engine.Use(gin.Logger())
// Recovery middleware recovers from any panics and writes a 500 if there was one.
engine.Use(gin.Recovery())
v1Group := engine.Group("app/v1", accessHandler())
v1Group.GET("user/info", userInfoLogic())
engine.Run(":8019")
}
終端執行go run main.go
,輸出如下
$ go run main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /app/v1/user/info --> main.userInfoLogic.func1 (4 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8019
透過列印可以看出註冊了GET方法的路由/app/v1/user/info
,對應處理函式為main.userInfoLogic
,
總共包括四個處理函式,按順序為gin.Logger()
、gin.Recovery()
、accessHandler()
以及userInfoLogic
最終在埠8019啟動了HTTP監聽服務。
2.1 建立Engine
並使用gin.Logger()
和 gin.Recovery()
兩個全域性中介軟體,對engine
下的所有路由都生效
透過程式碼及註釋,gin.Logger()
和 gin.Recovery()
放到了Engine.RouterGroup.Handlers
切片中。
// Use attaches a global middleware to the router. i.e. the middleware attached through Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
2.2 建立路由分組v1Group
,且該分組使用了accessHandler()
,accessHandler()
對v1Group
分組路由均生效
// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
// For example, all the routes that use a common middleware for authorization could be grouped.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{
Handlers: group.combineHandlers(handlers),
basePath: group.calculateAbsolutePath(relativePath),
engine: group.engine,
}
}
從程式碼可以看出,返回了新的gin.RouterGroup
,並且
v1Group.Handlers = append(group.Handlers, handlers)
,此時gin.RouterGroup.Handlers
為[gin.Logger(),gin.Recovery(),accessHandler()]
同時v1Group.basePath = "app/v1"
從程式碼同時可以得出,支援分組巢狀分組。即在v1Group
都基礎上在建立分組,比如v1Group.Group("north")
2.3 在v1Group
下注冊路由user/info
,該路由的處理函式是userInfoLogic
,方法為GET
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) {
...
root := engine.trees.get(method)
if root == nil {
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)
...
}
將分組v1Group
的路由字首和當前user/info
計算得到完整路由,即app/v1/user/info
合併處理函式,此時handlers = [gin.Logger(),gin.Recovery(),accessHandler(),userInfoLogic()]
最後將路由及處理函式按http method分組,加入到不同路由樹中。
2.4 透過 engine.Run(":8019")
在啟動HTTP服務
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
...
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler())
return
}
這裡呼叫http.ListenAndServe
啟動HTTP
監聽服務,Engine
實現了http.Handler
介面,如果有客戶端請求,會呼叫到Engine.ServeHTTP
函式。
3. 路由過程
// gin.go
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
...
// Find root of the tree for the given HTTP method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// Find route in tree
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
...
break
}
}
// context.go
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
從上面程式碼可以看出,透過http method找到對應的路由樹,再根據URL從路由樹中查詢對應的節點,
獲取到處理函式切片,透過c.Next
按透過順序執行處理函式。
對於請求GET /app/v1/user/info,將依次執行[gin.Logger(),gin.Recovery(),accessHandler(),userInfoLogic()]
4. 請求/響應引數處理
func accessHandler() func(*gin.Context) {
return func(c *gin.Context) {
// 不允許crul訪問
if strings.Contains(c.GetHeader("user-agent"), "curl") {
c.JSON(http.StatusBadRequest, "cant't not visited by curl")
c.Abort() // 直接退出,避免執行後續處理函式
}
}
}
func userInfoLogic() func(*gin.Context) {
return func(c *gin.Context) {
id := c.Query("id")
c.JSON(http.StatusOK, map[string]interface{}{"id": id, "name": "bob", "age": 18})
}
}
v1Group
的通用處理函式accessHandler
,達到v1Group
下注冊的路由無法用curl訪問的效果。
透過c.Query("id")
獲取URL查詢引數,
透過以下程式碼可以看出,第一次獲取URL查詢時會快取所有URL查詢引數,這減少了記憶體的分配,節省了計算資源。
因為每次呼叫url.ParseQuery
都會重新申請快取,重複解析URL。
func (c *Context) Query(key string) (value string) {
value, _ = c.GetQuery(key)
return
}
func (c *Context) initQueryCache() {
if c.queryCache == nil {
if c.Request != nil {
c.queryCache = c.Request.URL.Query()
} else {
c.queryCache = url.Values{}
}
}
}
func (c *Context) GetQueryArray(key string) (values []string, ok bool) {
c.initQueryCache()
values, ok = c.queryCache[key]
return
}
透過c.JSON
返回Content-Type
為application/json
的響應體,
這也是Gin
對原生net/http程式設計的一個最佳化,對常用的響應型別進行封裝,方便使用者使用。
當然,Gin對請求/響應引數的處理還有其它很多細微的最佳化,這裡就不詳細說明了。
5. 總結
Gin使用Map來實現路由匹配,而Gin使用路由樹來實現路由匹配,支援動態路由,記憶體佔用小且路由匹配快。
同時Gin使用快取來最佳化請求引數的處理過程,提供了通用的響應引數處理等,方便使用者使用。