golang框架-web框架之gin

1,848 阅读5分钟

gin介绍

gin是一个 Web应用框架,拥有良好的性能和简单明了的接口。同时支持中间件,类型绑定等实用功能。

为什么要用gin

在实际开发中,很少会直接实用http.Server。而自己搭建框架有一定成本,同时没有经过系统的校验,容易出现问题。而现有的框架中,gin拥有良好的性能,更重要的是接口清晰明了,接入成本极低。同时,其支持的功能也是多种多样,如中间件,类型绑定,日志规范。

gin 性能

以下是从官网拿到的性能对比指标表

  • (1): Total Repetitions achieved in constant time, higher means more confident result
  • (2): Single Repetition Duration (ns/op), lower is better
  • (3): Heap Memory (B/op), lower is better
  • (4): Average Allocations per Repetition (allocs/op), lower is better
Benchmark name (1) (2) (3) (4)
BenchmarkGin_GithubAll 30000 48375 0 0
BenchmarkAce_GithubAll 10000 134059 13792 167
BenchmarkBear_GithubAll 5000 534445 86448 943
BenchmarkBeego_GithubAll 3000 592444 74705 812
BenchmarkBone_GithubAll 200 6957308 698784 8453

接入栗子

简单示例

接受/ping路径的Get请求,并返回message:"pong"

package main

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

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

支持所有的http协议

router := gin.Default()
router.GET("/someGet", getting)
router.POST("/somePost", posting)
router.PUT("/somePut", putting)
router.DELETE("/someDelete", deleting)
router.PATCH("/somePatch", patching)
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)

参数解析

router := gin.Default()
// The request responds to a url matching:  /welcome?firstname=Jane&lastname=Doe
/**** example1: 解析Querystring ****/
router.GET("/welcome", func(c *gin.Context) {
    firstname := c.DefaultQuery("firstname", "Guest")
    // shortcut for c.Request.URL.Query().Get("lastname")
    lastname := c.Query("lastname") 
    c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})

/**** example2: 解析表单 ****/
router.POST("/form_post", func(c *gin.Context) {
	message := c.PostForm("message")
	nick := c.DefaultPostForm("nick", "anonymous")

	c.JSON(200, gin.H{
	    "status":  "posted",
		"message": message,
		"nick":    nick,
	})
})

gin特性

路由组

支持以组为单位的路由,下面栗子就是以/v1开头,以/v2开头的两组配置。路由组可以共享同样的配置,比如路由组v1可以使用中间件a。而v2可以使用另一个中间件,互不影响。

router := gin.Default()

// Simple group: v1
v1 := router.Group("/v1")
{
	v1.POST("/login", loginEndpoint)
	v1.POST("/submit", submitEndpoint)
	v1.POST("/read", readEndpoint)
}

// Simple group: v2
v2 := router.Group("/v2")
{
	v2.POST("/login", loginEndpoint)
	v2.POST("/submit", submitEndpoint)
	v2.POST("/read", readEndpoint)
}
router.Run(":8080")

中间件

中间件是对框架能力一个非常好的抽象。以组件的形式,为路由或路由组提供插件式功能。也可以自己实现中间件,加入到Use中来。

// Creates a router without any middleware by default
r := gin.New()

// Global middleware
r.Use(gin.Logger())
r.Use(gin.Recovery())

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

中间件可以非常方便的定义日志格式

router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
    // your custom format
	return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
			param.ClientIP,
			param.TimeStamp.Format(time.RFC1123),
			param.Method,
			param.Path,
			param.Request.Proto,
			param.StatusCode,
			param.Latency,
			param.Request.UserAgent(),
			param.ErrorMessage,
		)
}))

数据绑定

使用 c.ShouldBind方法,可以将参数自动绑定到 struct.该方法是会检查 Url 查询字符串和 POST 的数据,而且会根据 content-type类型,优先匹配JSON或者 XML,之后才是 Form.

// 定义一个 Person 结构体,用来绑定数据
type Person struct {
    Name     string    `form:"name"`
    Address  string    `form:"address"`
    Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
}

func main() {
    route := gin.Default()
    route.GET("/testing", startPage)
    route.Run(":8085")
}

func startPage(c *gin.Context) {
    var person Person
    // 绑定到 person
    if c.ShouldBind(&person) == nil {
        log.Println(person.Name)
        log.Println(person.Address)
        log.Println(person.Birthday)
    }

    c.String(200, "Success")
}

gin原理分析

gin 可以说全是在handler上做文章。下面我们就以这三句话,一探gin。

func main() {
    r := gin.Default()
    r.GET("/getb", GetDataB)
    r.Run()
}

生成默认引擎

r := gin.Default() 的定义如下

 func Default() *Engine {
    debugPrintWARNINGDefault()
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
}

Engine是gin中的一个很重要的概念。等下面对.r.Run分析时候,我们会发现他的本质就是http.Server里面的handler实例!

这里看到engine.Use(Logger(), Recovery()) 直观上就很像之前提前的http中间件的某种实现 Logger()是日志中间件, Recovery()是针对panic的中间件(不然每个handler都得写个panic处理逻辑)

来分析一下Recover中间件

func Recovery() HandlerFunc {
	return RecoveryWithWriter(DefaultErrorWriter)
}

func RecoveryWithWriter(out io.Writer) HandlerFunc {
	var logger *log.Logger
	if out != nil {
		logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
	}
	return func(c *Context) {
		defer func() {
			if err := recover(); err != nil {
               // 省略非关键代码
			}
		}()
		c.Next()
	}
}

c.Next()能将多个中间件串联起来调用

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

c.handlersc.index即当前索引位置对应的handler的调用

type HandlersChain []HandlerFunc
type HandlerFunc func(*Context)

由于处理逻辑是放在了c.Next前面,所以中间件的处理顺序是先入后出。中间件本身应该互相独立。但如果因为特殊原因,有前后依赖,就要注意这点。

注册路由规则

r.GET("/getb", GetDataB) 实现如下

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

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

可以看到,路由映射是加入了一颗树中。这里使用的是radix树,Radix树,即基数树,也称压缩前缀树,是一种提供key-value存储查找的数据结构。与Trie不同的是,它对Trie树进行了空间优化,只有一个子节点的中间节点将被压缩。同样的,Radix树的插入、查询、删除操作的时间复杂度都为O(k)。存储原理示意图如下:

启动服务

看下r.Run的实现

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
}

可以看到,实际调用的,还是http.ListenAndServe这个方法,engine作为handler参数传入。http.ListenAndServe的原理,在前文httpServer有过阐述,这里不做过多分析。

总结

gin有如下特点:

  • 接入成本非常低,作为一个组件,这是最重要的一点
  • 拥有强大的中间件功能,用户可以自主定制需要的功能
  • 由于使用了radix树,路由的性能很高。
  • 数据绑定,让用户可以非常方便地从请求中获取想要的结构体。

gin对外接口和代码实现都非常优秀,无论是项目使用,还是源码学习,都值得推荐。