前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Gin框架详解

Gin框架详解

原创
作者头像
榴莲其实还可以
发布2019-07-19 18:18:38
7.1K0
发布2019-07-19 18:18:38
举报

1,前言

gin是一个开源的,用golang开发的web框架,https://github.com/gin-gonic/gin 地址如下。它有如下特性:

1,快,基于前缀树(radix tree)的路由策略,占用更小的内存,无须反射。

2,支持中间件,对于一个http请求,可以通过一串链式的中间件处理,然后再作出最后的应答。

3,crash-free,不会crash停服,Gin框架可以捕获http请求中的panic,并恢复。

4,routes group。更好的组织你的路由

5,内置的rendering,gin提供了简单的api使用json,xml,html,pb等

6,扩展性,可以简单的创建自己的中间件

2,一些简单的示例与说明

2.1 示例1

代码语言:javascript
复制
package main

import "github.com/gin-gonic/gin"

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run() // listen and serve on 0.0.0.0:8080
}

示例1应该是最简单的一个例子了,通过http GET 方法访问 /ping 返回一个json格式的应答。简单的几行代码就可以搭建一个简单的可运行的cgi服务。

2.2 示例2

代码语言:javascript
复制
package main

import "github.com/gin-gonic/gin"

func main() {
	// Creates a router without any middleware by default
	r := 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
	r.Use(gin.Logger())

	// Recovery middleware recovers from any panics and writes a 500 if there was one.
	r.Use(gin.Recovery())

	// Per route middleware, you can add as many as you desire.
	r.GET("/benchmark", MyBenchLogger(), benchEndpoint)

	// Authorization group
	// authorized := r.Group("/", AuthRequired())
	// exactly the same as:
	authorized := r.Group("/")
	// per group middleware! in this case we use the custom created
	// AuthRequired() middleware just in the "authorized" group.
	authorized.Use(AuthRequired())
	{
		authorized.POST("/login", loginEndpoint)
		authorized.POST("/submit", submitEndpoint)
		authorized.POST("/read", readEndpoint)

		// nested group
		testing := authorized.Group("testing")
		testing.GET("/analytics", analyticsEndpoint)
	}

	// Listen and serve on 0.0.0.0:8080
	r.Run(":8080")
}

示例2,将服务挂在8080端口下,这个示例涉及到中间件,route group。相对来说会复杂点。以中间件来说,当匹配到“/benchmark” 这个cgi的时候,他的处理会经过一个链式的处理,这个http请求到来的时候会先后经过,gin.Logger(),gin.Recover(),MyBenchLoggerer()以及最后的benchEndpoint的处理。对于下面的route group,则是一个group下的cig,共有一个middleware。示例的意思就是说在调用这些cgi的handler之前,都会先调用到鉴权服务authRequired.

3,源码剖析

3.1 Use 添加middleware

代码语言:javascript
复制
// Use attaches a global middleware to the router. ie. the middleware attached though 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()
}

type Engine struct {
	RouterGroup
	.......
}

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

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

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

Use调用就是构造了一个HandlersChain,并保存在RouterGroup 结构中。

3.2 相关cgi处理GET/POST/PUT....

以GET为例

代码语言:javascript
复制
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle("GET", relativePath, handlers)
}

// POST is a shortcut for router.Handle("POST", path, handle).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle("POST", 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 (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
	finalSize := len(group.Handlers) + len(handlers)
	if finalSize >= int(abortIndex) {
		panic("too many handlers")
	}
	mergedHandlers := make(HandlersChain, finalSize)
	copy(mergedHandlers, group.Handlers)
	copy(mergedHandlers[len(group.Handlers):], handlers)
	return mergedHandlers
}

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)
	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)
}

// addRoute adds a node with the given handle to the path.
// Not concurrency-safe!
func (n *node) addRoute(path string, handlers HandlersChain) {
	..............
}

当调用GET/POST的时候,都会调用到handle(...),该调用又两个地方比较重要:

1,combineHandlers,该调用将之前Use中添加的middleware与此处GET/POST中的handler 合并起来了。并且其顺序是middleware在前。同时我们也可以看到,只有在GET调用之前的middleware,在对应的http请求到来时才会被调用到。举个例子:

代码语言:javascript
复制
r.Use(A)
r.GET("/ping",B)
r.Use(C)

如果调用顺序是这样的,那么GET请求/ping 这个cgi的时候,请求会经过A->B的处理,不会经过C.

2,addRoute,该调用是将对应的请求挂到engine.trees上,每类请求方式(GET/POST..)一颗前缀树。使用前缀树(radix tree)可以节省存储空间,以及提高查找效率。需要说明的是,添加前缀树节点的时候是线程不安全的。

3.3 Run

代码语言:javascript
复制
// 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) {
	defer func() { debugPrintError(err) }()

	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	err = http.ListenAndServe(address, engine)
	return
}

func ListenAndServe(addr string, handler Handler) error {
  ..............
}

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

http.ListenAndServe的内部是go自身的网络框架调用,这里就不深入下去。我们可以看到Run中ListenAndServe传入的是engine,而需要的的参数是Handler类型,显然我们知道,这个网络框架不论内部怎么调用,最后都会回调到func(engine *Engine)ServeHTTP()这个函数中。

代码语言:javascript
复制
// 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)
}

func (engine *Engine) handleHTTPRequest(c *Context) {
	...................
	...................
	// 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, unescape)
		if value.handlers != nil {
			c.handlers = value.handlers
			c.Params = value.params
			c.fullPath = value.fullPath
			c.Next()
			c.writermem.WriteHeaderNow()
			return
		}
		...............
	}
}

// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
	c.index++
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)
		c.index++
	}
}

gin.Context主要是用来做参数传递的。handleHTTPRequest这里就是整个调用的关键了。首先会根据http方法(GET/POST..)找到对应的前缀树。然后再通过http请求的路径(比如/ping),找到对应的节点(nodeValue),这个节点里面就保存了前面添加的middleware以及最后的处理函数。

我们看这个c.Next(),它会从当前位置开始,遍历整个handlers,然后调用相应的函数(c.handlers[c.index](c)).

4, 回包

回包一般都是调用链的最后一个函数来进行回包,比如示例1中的代码:

代码语言:javascript
复制
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})

回包就是GET中的匿名函数回包的,返回一个json应答。

代码语言:javascript
复制
// JSON serializes the given struct as JSON into the response body.
// It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) {
	c.Render(code, render.JSON{Data: obj})
}

// XML serializes the given struct as XML into the response body.
// It also sets the Content-Type as "application/xml".
func (c *Context) XML(code int, obj interface{}) {
	c.Render(code, render.XML{Data: obj})
}

// YAML serializes the given struct as YAML into the response body.
func (c *Context) YAML(code int, obj interface{}) {
	c.Render(code, render.YAML{Data: obj})
}

// ProtoBuf serializes the given struct as ProtoBuf into the response body.
func (c *Context) ProtoBuf(code int, obj interface{}) {
	c.Render(code, render.ProtoBuf{Data: obj})
}

// Render writes the response headers and calls render.Render to render data.
func (c *Context) Render(code int, r render.Render) {
	c.Status(code)

	if !bodyAllowedForStatus(code) {
		r.WriteContentType(c.Writer)
		c.Writer.WriteHeaderNow()
		return
	}

	if err := r.Render(c.Writer); err != nil {
		panic(err)
	}
}

// Render (JSON) writes data with custom ContentType.
func (r JSON) Render(w http.ResponseWriter) (err error) {
	if err = WriteJSON(w, r.Data); err != nil {
		panic(err)
	}
	return
}

我们可以看到,c.JSON(),最后会通过WriteJSON调用回包。此外,我们也可以看到gin内置的应答格式还是非常多的。原生支持json,xml,yaml和pb以及其它的一些格式。

结语

目前部门内的web框架都是基于java的jungle框架,比较老了。go作为后起之秀,得益于其语言与性能的优势,目前部门内新服务都是用go开发,需要重构的老服务也在用go迁移重构。对于go的web框架,目前再调研中,故将gin框架大体熟悉了解了下。作文记录下。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1,前言
  • 2,一些简单的示例与说明
    • 2.1 示例1
      • 2.2 示例2
      • 3,源码剖析
        • 3.1 Use 添加middleware
          • 3.2 相关cgi处理GET/POST/PUT....
            • 3.3 Run
            • 4, 回包
            • 结语
            相关产品与服务
            消息队列 TDMQ
            消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档