4

Gin使用及源码简析 - Amos01

 1 year ago
source link: https://www.cnblogs.com/amos01/p/17125045.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1. Gin简介

前面通过两篇文章分享了Golang HTTP编程的路由分发、请求/响应处理。

可以看出来Golang原生HTTP编程在路由分组、动态路由及参数读取/验证、构造String/Data/JSON/HTML响应的方法等存在优化的空间。

Gin是一个用Golang编写的高性能Web框架。

  • 基于前缀树的路由,快速且支持动态路由
  • 支持中间件及路由分组,将具有同一特性的路由划入统一组别、设置相同的中间件。
    • 比如需要登录的一批接口接入登录权限认证中间件、而不需要登录一批接口则不需要接入

2. 快速使用

基于[email protected],基本使用如下

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.gofunc (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.gofunc (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-Typeapplication/json的响应体,

这也是Gin对原生net/http编程的一个优化,对常用的响应类型进行封装,方便使用者使用。

当然,Gin对请求/响应参数的处理还有其它很多细微的优化,这里就不详细说明了。

Gin使用Map来实现路由匹配,而Gin使用路由树来实现路由匹配,支持动态路由,内存占用小且路由匹配快。

同时Gin使用缓存来优化请求参数的处理过程,提供了通用的响应参数处理等,方便用户使用。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK