Gin框架详解

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

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

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

// 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为例

// 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请求到来时才会被调用到。举个例子:

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

// 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()这个函数中。

// 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中的代码:

	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})

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

// 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框架大体熟悉了解了下。作文记录下。

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

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

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券