4

golang框架-web框架之gin

 3 years ago
source link: https://studygolang.com/articles/34370?fr=sidebar
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.
neoserver,ios ssh client

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_GithubAll300004837500BenchmarkAce_GithubAll1000013405913792167BenchmarkBear_GithubAll500053444586448943BenchmarkBeego_GithubAll300059244474705812BenchmarkBone_GithubAll20069573086987848453

接受/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)。存储原理示意图如下:

757e897ef9003bff59cc7fcbce76075a.png

看下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对外接口和代码实现都非常优秀,无论是项目使用,还是源码学习,都值得推荐。


有疑问加站长微信联系(非本文作者)

280

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK