gin学习笔记(一)—— 了解gin

发布时间 2024-01-12 00:02:26作者: 昨晚没做梦

了解gin


Web编程基础

客户端和服务端

HTTP

  客户端和服务器之间的请求响应一般都是使用 HTTP/HTTPS 协议,它规定了如何从网站服务器传输超文本到本地浏览器。

HTTP请求

  当你在网页上点击一个链接、提交一个表单、或者进行一次搜索的时候,浏览器会发送一个 HTTP 请求给服务器。HTTP 请求有以下几种方式:

  • GET请求 —— 获取资源
  • POST请求 —— 发送、提交资源
  • PUT请求 —— 更新资源
  • DELETE请求 —— 删除资源
  • HEAD请求 —— 获取报头,检测资源是否存在
  • OPTIONS请求 —— 询问服务器所支持的HTTP请求方法
  • PATCH请求 —— 局部更新资源
  • CONNECT请求 —— 将连接改为管道方式的代理服务器
  • TRACE请求 —— 回显服务器收到的请求,主要用于测试或诊断

关于HTTP请求还有很多内容,其中重要的是GET、POST、PUT、HEAD和DELETE,这里有个大概了解就行。

HTTP响应

  HTTP 响应由四个部分组成, 分别是: 状态码、 响应报头、 空行、 响应正文

响应状态代码有三位数字组成, 第一个数字定义了响应的类别, 且有五种可能取值:

  • 100~199: 表示服务器成功接收部分请求, 要求客户端继续提交其余请求才能完成整个处理过程。
  • 200~299: 表示服务器成功接收请求并已完成整个处理过程。 常用 200(OK 请求成功)。
  • 300~399: 为完成请求, 客户需进一步细化请求。 例如: 请求的资源已经移动一个新地址、 常用 302(所请求的页面已经临时转移至新的 url) 、 307 和 304(使用缓存资源)。
  • 400~499: 客户端的请求有错误, 常用 404(服务器无法找到被请求的页面) 、 403(服务器拒绝访问, 权限不够)。
  • 500~599: 服务器端出现错误, 常用 500(请求未完成。 服务器遇到不可预知的情况)。

 

gin简介

  gin 是基于 httprouter 开发、使用 Go 语言编写的 Web 框架,实现动态路由,具有类似 martini 的 API,源码注释比较明确,具有快速灵活,容错方便等特点

特性

  • 快速

基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能。

  • 支持中间件

传入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如:Logger,Authorization,GZIP,最终操作 DB。

  • Crash 处理

Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic!

  • JSON 验证

Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。

  • 路由组

更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能。

  • 错误管理

Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送。

  • 内置渲染

Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API。

  • 可扩展性

 

第一个gin程序

package main

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

func main() {

    r := gin.Default() //创建默认的路由引擎

    //配置路由,即配置url路径和回调函数
    r.GET("/", func(c *gin.Context) {
        c.String(200, "这里是%v", "gin")
    })
    r.GET("/hello", func(c *gin.Context) {
        c.String(200, "Hello world!")
    })
    r.POST("/post", func(c *gin.Context) { //说明可以用post请求访问 /post
        c.String(200, "post请求的回调函数")
    })
    r.PUT("/put", func(c *gin.Context) { //说明可以用post请求访问 /put
        c.String(200, "put请求的回调函数")
    })
    r.DELETE("/delete", func(c *gin.Context) { //说明可以用post请求访问 /delete
        c.String(200, "delete请求的回调函数")
    })

    //启动 http 服务,默认在 0.0.0.0:8080 启动
    r.Run(":8000") //可以手动设置端口
}

  运行程序后,在浏览器输入 127.0.0.1:8000 和 127.0.0.1:8000/hello

   但是,当我们输入其他几个,如 127.0.0.1:8000/post 时,会出现 404 错误,这是因为通过浏览器的地址栏输入地址,所访问的URL都是get请求。我们可以使用 postman 进行其他请求:

 

gin路由

路由是指确定应用程序如何响应客户端对特定端点的请求,该特定端点是URI(或路径)和特定的HTTP请求方法(GET,POST等),根据URI上的路径,指引该条请求到对应的方法里去执行然后返回,中间可能会执行一些中间件。

每个路由可以具有一个或多个处理程序函数,这些函数在匹配该路由时执行。

静态路由和动态路由

  • 静态路由: 框架/用户提前生成一个路由表,一般是map结构,key为URL上的path,value为代码执行点(处理函数),

    • 优点:只需要读取map,没有任何开销,速度奇快
    • 缺点:无法正则匹配路由,只能逐一对应,模糊匹配的场景无法使用
  • 动态路由: 用户定义好路由匹配规则,框架匹配路由时,根据规则动态的去规划路由

    • 优点:适应性强,解决了静态路由的缺点
    • 缺点:相比静态路由有开销,具体视算法和路由匹配规则而定

gin框架就是动态路由。

 

gin路由基础使用

//普通路由
r.GET("/hello", func(c *gin.Context) {...})
r.POST("/hello", func(c *gin.Context) {...})
r.PUT("/put", func(c *gin.Context) {...})

//可以匹配所有请求方法的路由
r.any("/test", func(c *gin.Context) {...})

 

路由组

  我们可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}包裹同组的路由,这只是为了看着清晰,你用不用{}包裹功能上没什么区别。

func main() {
    r := gin.Default()
    userGroup := r.Group("/user")
    {
        userGroup.GET("/index", func(c *gin.Context) {...})
        userGroup.GET("/login", func(c *gin.Context) {...})
        userGroup.POST("/login", func(c *gin.Context) {...})

    }
    shopGroup := r.Group("/shop")
    {
        shopGroup.GET("/index", func(c *gin.Context) {...})
        shopGroup.GET("/cart", func(c *gin.Context) {...})
        shopGroup.POST("/checkout", func(c *gin.Context) {...})
    }
    r.Run()
}

  通常我们将路由分组用在划分业务逻辑或划分API版本时。

 

路由原理

  Gin框架中的路由使用的是 httprouter 库,其基本原理就是构造一个路由地址的压缩前缀树

  前缀树的每一个节点代表一个字符串(前缀)。每一个节点会有多个子节点,通往不同子节点的路径上有着不同的字符。子节点代表的字符串是由节点本身的原始字符串,以及通往该子节点路径上所有的字符组成的。

举个例子:

r.GET("/", func(c *gin.Context) {...})
r.GET("/golang", func(c *gin.Context) {...}) r.GET("/golala", func(c *gin.Context) {...}) r.GET("/go", func(c *gin.Context) {...})
r.GET("/go/la", func(c *gin.Context) {...})

gin 的压缩前缀树变化如下:

  gin 会为每种不同的请求方法构造相互独立的压缩前缀树,路由注册的过程是构造压缩前缀树的过程,路由匹配的过程就是查找压缩前缀树的过程。

 

中间件

  中间件是一种特殊的处理函数,它在路由的处理函数执行前或执行后运行,常用于处理一些通用的任务,例如日志记录、错误处理、用户身份验证等。

  Gin中的中间件必须是一个 gin.HandlerFunc 类型。也就是参数为 *gin.Context 的 functionValue (函数类型):

type HandlerFunc func(*Context)

中间件的时机

  1. 当一个HTTP请求到达时,Gin会按照注册的顺序调用每个中间件,对请求进行一些处理,例如检查请求头或请求体的内容、数据过滤等。
  2. 处理完请求后响应时,调用中间件处理,例如统一添加响应头、数据过滤等
  3. 中间件可以选择继续传递请求到下一个中间件,或者直接结束请求的处理并返回响应。

 

定义中间件

  通过一个简单的例子来了解中间件:

//方法一

func
SimpleLogger() gin.HandlerFunc { return func(c *gin.Context) { t := time.Now() // 在请求被处理之前,记录一些信息 log.Printf("before request: %s\n", c.Request.URL.Path) // 调用下一个中间件或处理函数 c.Next() // 在请求被处理之后,记录一些信息 log.Printf("after request: %s, elapsed time: %v\n", c.Request.URL.Path, time.Since(t)) } }

  当然,也可以这样:

//方法二

func Logger(c *gin.Context) {
    t := time.Now()

    // 在请求被处理之前,记录一些信息
    log.Printf("before request: %s\n", c.Request.URL.Path)

    // 调用下一个中间件或处理函数
    c.Next()

    // 在请求被处理之后,记录一些信息
    log.Printf("after request: %s, elapsed time: %v\n", c.Request.URL.Path, time.Since(t))
}

 

注册中间件

  可以调用 Use 方法注册中间件:

r.Use(SimpleLogger())  //对应上面方法一
r.Use(Logger)  //对应方法二

全局注册

  全局注册的中间件 会应用于所有路由: 会应用于所有在中间件后的路由:

func main() {

    r := gin.Default()

    r.GET("/", func(c *gin.Context) {...})
    r.GET("/golang", func(c *gin.Context) {...})  //这两个都不会触发中间件

  r.Use(SimpleLogger())
r.POST(
"/golang", func(c *gin.Context) {...})  //这些会触发 r.PUT("/go", func(c *gin.Context) {...}) r.Run() }

路由组注册

  路由组注册的中间件会应用于路由组内所有路由:

//写法一
shopGroup := r.Group("/shop", SimpleLogger())
{
    shopGroup.GET("/index", func(c *gin.Context) {...})
    ...
}


//写法二
shopGroup := r.Group("/shop")
shopGroup.Use(SimpleLogger())
{
    shopGroup.GET("/index", func(c *gin.Context) {...})
    ...
}

单个路由注册

r.GET("/golang", SimpleLogger(), func(c *gin.Context) {...})

 

中间件工作流程

中间件的执行模型是洋葱模型:

  像剥洋葱一样从最外层(最先注册)执行到最内层(最后注册)。

中间件之间通过 Context.Next() 来传递请求,也可以使用 Context.Abort()  直接结束请求

  在了解Next和Abort之前,先看一下 Context 的部分数据结构:

//context.go
type Context struct {
    ...
    handlers HandlersChain  //函数切片
    index    int8  //函数切片的索引
    ...
}

//gin.go
type HandlersChain []HandlerFunc

Context.Next()

  主要工作就是将 c.index++,然后 c.handlers[c.index](c),执行内层的函数,即剥下一层洋葱。

源码文件:gin/context.go    line:171

func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}

  注意这里的 for 循环,它可以保证请求方法的执行,例如:中间件二没有添加 c.Next(),那么中间件一的 for 也能继续内层执行。是非常微妙的操作,这里还有一些细节在下方思考中描述。

 

Context.Abort()

  主要作用就是将 index 设置为 63,然后结束请求,也就是直接不剥洋葱了。

源码文件:context.go    line:47188

const abortIndex int8 = math.MaxInt8 >> 1

func (c *Context) Abort() {
    c.index = abortIndex    //63
}

  为什么设置为 63 就能结束?这是因为 handlers 的长度必须小于 63,否则在注册时就会直接 panic.;所以 63 是超出长度的,不会执行任何函数然后结束。 

 

中间件注意事项以及思考

注意事项

  • gin默认中间件

gin.Default() 默认使用了 Logger 和 Recovery 中间件,其中:

  1. Logger 中间件将日志写入 gin.DefaultWriter,即使配置了 GIN_MODE=release。
  2. Recovery 中间件会 recover 任何 panic。如果有 panic 的话,会写入500响应码。

如果不想使用上面两个默认的中间件,可以使用 gin.New() 新建一个没有任何默认中间件的路由。

  • gin中间件中使用 goroutine

当在中间件或 handler 中启动新的 goroutine 时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy()):

r.GET("/", func(c *gin.Context) {

    cCp := c.Copy()

    go func() {
        fmt.Println("Done! in path " + cCp.Request.URL.Path)
    }()

    c.String(200, "首页")

})

 

思考

get请求后,以下程序会输出什么?为什么?

func m1(c *gin.Context) {
    log.Printf("m1-start")
    c.Next()
    log.Printf("m1-end")
}

func m2(c *gin.Context) {
    log.Printf("m2-start")
    //c.Next()
    log.Printf("m2-end")
}

func m3(c *gin.Context) {
    log.Printf("m3-start")
    //c.Next()
    log.Printf("m3-end")
}

func main() {
    r := gin.Default()
    r.Use(m1)
    r.Use(m2)
    r.Use(m3)

    r.GET("/golang", func(c *gin.Context) {
     log.Printf("/golang") c.String(
200, "Hello world!") })
  r.Run() }
m1-start
m2-start
m2-end
m3-start
m3-end
/golang
m1-end
输出结果

答:因为 c.Next() 有 for 循环保证执行,m2和m3都没有 c.Next(),因此没办法层层嵌套,都在 m1 的 for 循环中逐个执行了。

 

 

如果上题中的中间件都不使用c.Next(),会发生什么?为什么会这样?

m1-start
m1-end
m2-start
m2-end
m3-start
m3-end
/golang
输出结果

 

答:这是因为如果有请求到达,那么 r.Run() 里会调用 c.Next()。其调用关系大致如下:

  1. 请求到达时的处理在 gin/gin.go 中的 handleHTTPRequest 方法中,里面有调用 c.Next();
  2. handleHTTPRequest 是 ServerHTTP  方法(gin/gin.go中)调用的 ;
  3. 而 ServerHTTP 是 Handler  接口要求的函数;
  4. r.Run() (gin/gin.go中)有 Handler 接口变量

 

小结

  这一篇中,我们学习了路由和中间件的基础使用,大致了解其工作原理,压缩前缀树的图解和中间件的工作流程是本篇的重点。

  中间件和路由是gin框架学习的非常重要的地方,这里只是简单描述,在之后还会涉及到。