Go语言web框架——Gin

Gin

Gin是一个go语言写的Web框架

1 Web工作流程

  • 客户机通过TCP/IP协议建立到服务器的TCP连接
  • 客户端向服务器发送HTTP协议请求 Request GET /url,请求服务器里的资源文档
  • 服务器向客户机发送HTTP协议应答Response,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端
  • 客户机与服务器断开。由客户端解释HTML文档,在客户端屏幕上渲染图形结果

img

2 Gin 框架

Go 语言最流行了两个轻量级 Web 框架分别是 Gin 和 Echo,这两个框架大同小异,都是插件式轻量级框架,背后都有一个开源小生态来提供各式各样的小插件,这两个框架的性能也都非常好,裸测起来跑的飞快。Gin 起步比 Echo 要早,市场占有率要高一些,生态也丰富一些。

参考优秀文章:

2.1 Hello World

Gin 运行的大致过程为:

  1. 实例化一个 Engine 结构体
  2. 通过 net/http 库把 Engine 实例和 http.Server 进行绑定
  3. net/http 启动监听服务
  4. 接到请求将请求转发到 Engine 的 ServeHTTP 接口,并调用 handleHTTPRequest 进行请求处理
  5. handleHTTPRequest 对 path 进行处理,并通过查询 trees 的节点,得到请求 handlers
  6. handlers 被交给 Context 进行真正的 handler 调用并返回请求结果
func main() {
    
    
   // 初始化一个http服务对象
   engine := gin.Default()
   // 设置一个get请求的路由,url为localhost
   engine.GET("/", func(c *gin.Context) {
    
    
      c.String(http.StatusOK, "hello World!")
   })
    //监听并启动服务,默认 http://localhost:8080/
   engine.Run()
}

engine.Run()

// 将 Engine 的路由挂载到 http.Server 上,并开起监听,等待 HTTP 请求
//     addr:监听地址
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
}

2.2 gin.Engine

Engine 是 Gin 框架最重要的数据结构,它是框架的入口。我们通过 Engine 对象来定义服务路由信息、组装插件、运行服务。整个 Web 服务的都是由它来驱动的。底层的 HTTP 服务器使用的是 Go 语言内置的 http server,Engine 的本质只是对内置的 HTTP 服务器的包装,让它使用起来更加便捷。

gin.Default()

// 返回一个启用 Recovery 中间件 和 Logger 中间件的 Engine 实例
// Logger 用于输出请求日志,Recovery 确保单个请求发生 panic 时记录异常堆栈日志,输出统一的错误响应。
func Default() *Engine {
    
    
    debugPrintWARNINGDefault()
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
}

请求响应流程:

  • 客户端请求服务器
  • HTTP 服务引擎 http.Server 接收到请求,并初步处理成 http.ResponseWriter 和 *http.Request 并传递给注册过的上层请求处理 Handler(实现 ServerHTTP 接口,并注册到 http.ListenAndServe 的 Handler)(即 Gin 的 Engine)
  • Engine 把请求数据放入 Context pool 中,并传递给 Engine 的 handleHTTPRequest 进行处理
  • handleHTTPRequest 从 trees 中查找对应的 node,并回调注册过的请求处理 Handler

2.3 路由与控制器

路由是一个过程,指的是一个http请求,如何找到对应的处理器函数(也可以叫控制器函数),控制器函数主要负责执行http请求-响应任务。

r := gin.Default()

// 路由定义post请求, url路径为:/user/login, 绑定doLogin控制器函数
r.POST("/user/login", doLogin)

// 控制器函数
func doLogin(c *gin.Context) {
    
    
        // 获取post请求参数
	username := c.PostForm("username")
	password := c.PostForm("password")

	// 通过请求上下文对象Context, 直接往客户端返回一个字符串
	c.String(200, "username=%s,password=%s", username,password)
}

2.3.1 路由规则

一条路由规则由三部分组成:

  • http请求方法
    • GET
    • POST
    • PUT
    • DELETE
  • URL路径
  • 控制器函数

2.3.2 URL路径

echo框架,url路径有三种写法:

  • 静态url路径
  • 带路径参数的url路径
  • 带星号(*)模糊匹配参数的url路径
// 例子1, 静态Url路径, 即不带任何参数的url路径
/users/center
/user/111
/food/12

// 例子2,带路径参数的url路径,url路径上面带有参数,参数由冒号(:)跟着一个字符串定义。
// 路径参数值可以是数值,也可以是字符串

//定义参数:id, 可以匹配/user/1, /user/899 /user/xiaoli 这类Url路径
/user/:id

//定义参数:id, 可以匹配/food/2, /food/100 /food/apple 这类Url路径
/food/:id

//定义参数:type和:page, 可以匹配/foods/2/1, /food/100/25 /food/apple/30 这类Url路径
/foods/:type/:page

// 例子3. 带星号(*)模糊匹配参数的url路径
// 星号代表匹配任意路径的意思, 必须在*号后面指定一个参数名,后面可以通过这个参数获取*号匹配的内容。

//以/foods/ 开头的所有路径都匹配
//匹配:/foods/1, /foods/200, /foods/1/20, /foods/apple/1 
/foods/*path

//可以通过path参数获取*号匹配的内容。

2.3.3 控制器函数

控制器函数定义:

// 控制器函数接受一个上下文参数。可以通过上下文参数,获取http请求参数,响应http请求。
func HandlerFunc(c *gin.Context)
//实例化gin实例对象。
r := gin.Default()
	
//定义post请求, url路径为:/users, 绑定saveUser控制器函数
r.POST("/users", saveUser)

//定义get请求,url路径为:/users/:id  (:id是参数,例如: /users/10, 会匹配这个url模式),绑定getUser控制器函数
r.GET("/users/:id", getUser)

//定义put请求
r.PUT("/users/:id", updateUser)

//定义delete请求
r.DELETE("/users/:id", deleteUser)


//控制器函数实现
func saveUser(c *gin.Context) {
    
    
    ...忽略实现...
}

func getUser(c *gin.Context) {
    
    
    ...忽略实现...
}

func updateUser(c *gin.Context) {
    
    
    ...忽略实现...
}

func deleteUser(c *gin.Context) {
    
    
    ...忽略实现...
}

提示:实际项目开发中不要把路由定义和控制器函数都写在一个go文件,不方便维护,可以参考第一章的项目结构,规划自己的业务模块。

2.3.4 分组路由

路由分组,其实就是设置了同一类路由的url前缀。在做api开发的时候,如果要支持多个api版本,通过分组路由来实现api版本处理。

func main() {
    
    
	router := gin.Default()

	// 创建v1组
	v1 := router.Group("/v1")
	{
    
    
         // 在v1这个分组下,注册路由
		v1.POST("/login", loginEndpoint)
		v1.POST("/submit", submitEndpoint)
		v1.POST("/read", readEndpoint)
	}

	// 创建v2组
	v2 := router.Group("/v2")
	{
    
    
         // 在v2这个分组下,注册路由
		v2.POST("/login", loginEndpoint)
		v2.POST("/submit", submitEndpoint)
		v2.POST("/read", readEndpoint)
	}

	router.Run(":8080")
}

上面的例子将会注册下面的路由信息:

  • /v1/login
  • /v1/submit
  • /v1/read
  • /v2/login
  • /v2/submit
  • /v2/read

2.4 RouterGroup

RouterGroup 是对路由树的包装,所有的路由规则最终都是由它来进行管理。Engine 结构体继承了 RouterGroup ,所以 Engine 直接具备了 RouterGroup 所有的路由管理功能。同时 RouteGroup 对象里面还会包含一个 Engine 的指针。

type Engine struct {
    
    
  RouterGroup
  ...
}

type RouterGroup struct {
    
    
  ...
  engine *Engine
  ...
}

2.4.1 Engine 跟 RouterGroup 的关系

  • 对 Engine 的 trees(路由树) 进行管理
  • 对中间件进行管理
  • 对路由分组进行管理

由 RouterGroup 的实现代码可看出,它单独存在基本没有什么意义,它是一个专属于 Engine 的抽象抽象层,主要用于管理 Engine 的路由。

  • 由于 trees 的实体在 Engine 里,RouterGroup 要操作 trees,需要通过 Engine,所以 RouterGroup 里始终会有一个指向 Engine 实例的指针变量
  • 而 RouterGroup 的 Group 的接口和 root 变量的设计,则赋予了它为路由分组的能力

2.4.2 RouterGroup方法

RouterGroup 实现了 IRouter 接口,暴露了一系列路由方法,这些方法最终都是通过调用 Engine.addRoute 方法将请求处理器挂接到路由树中。

GET(string, ...HandlerFunc) IRoutes
POST(string, ...HandlerFunc) IRoutes
DELETE(string, ...HandlerFunc) IRoutes
PATCH(string, ...HandlerFunc) IRoutes
PUT(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes
// 匹配所有 HTTP Method
Any(string, ...HandlerFunc) IRoutes

2.4.3 路由注册流程

  • Engine 实例(已知 Engine 包含 RouterGroup)调用 GET,传递请求路径、响应函数方法
  • GET 调用 group.handle,
  • 并通过自身(Engine 里的 RouterGroup)的 Engine 指针,调用了 Engine 的 addRouter
  • 把请求路径、响应方法加入到 trees 的相应的 root 里

2.5 gin.Context

保存请求的上下文信息,它是所有请求处理器的入口参数。

type HandlerFunc func(*Context)

type Context struct {
    
    
  ...
  Request *http.Request // 请求对象
  Writer ResponseWriter // 响应对象
  Params Params // URL匹配参数
  ...
  Keys map[string]interface{
    
    } // 自定义上下文信息
  ...
}

Context 对象提供了非常丰富的方法用于获取当前请求的上下文信息,如果你需要获取请求中的 URL 参数、Cookie、Header 都可以通过 Context 对象来获取。这一系列方法本质上是对 http.Request 对象的包装。

// 获取 URL 匹配参数  /book/:id
func (c *Context) Param(key string) string
// 获取 URL 查询参数 /book?id=123&page=10
func (c *Context) Query(key string) string
// 获取 POST 表单参数
func (c *Context) PostForm(key string) string
// 获取上传的文件对象
func (c *Context) FormFile(name string) (*multipart.FileHeader, error)
// 获取请求Cookie
func (c *Context) Cookie(name string) (string, error) 
...

2.6 Gin框架运行模式

为方便调试,Gin 框架在运行的时候默认是debug模式,在控制台默认会打印出很多调试日志,上线的时候我们需要关闭debug模式,改为release模式。

设置Gin框架运行模式:

2.6.1 通过环境变量设置

export GIN_MODE=release

GIN_MODE环境变量,可以设置为debug或者release

2.6.2 通过代码设置

在main函数,初始化gin框架的时候执行下面代码

// 设置 release模式
gin.SetMode(gin.ReleaseMode)
// 或者 设置debug模式
gin.SetMode(gin.DebugMode)

3 Gin处理请求参数

3.1 获取Get 请求参数

Get请求url例子:*/path?id=1234&name=Manu&value=*111

获取Get请求参数的常用函数:

  • func (c *Context) Query(key string) string
  • func (c *Context) DefaultQuery(key, defaultValue string) string
  • func (c *Context) GetQuery(key string) (string, bool)

例子:

func Handler(c *gin.Context) {
    
    
	//获取name参数, 通过Query获取的参数值是String类型。
	name := c.Query("name")

        //获取name参数, 跟Query函数的区别是,可以通过第二个参数设置默认值。
        name := c.DefaultQuery("name", "tizi365")

	//获取id参数, 通过GetQuery获取的参数值也是String类型, 
	// 区别是GetQuery返回两个参数,第一个是参数值,第二个参数是参数是否存在的bool值,可以用来判断参数是否存在。
	id, ok := c.GetQuery("id")
        if !ok {
    
    
	   // 参数不存在
	}
}

提示:GetQuery函数,判断参数是否存在的逻辑是,参数值为空,参数也算存在,只有没有提交参数,才算参数不存在。

3.2 获取Post请求参数

  • 表单传输为post请求,http常见的传输格式为四种:
    • application/json
    • application/x-www-form-urlencoded
    • application/xml
    • multipart/form-data
  • 表单参数可以通过PostForm()方法获取,该方法默认解析的是x-www-form-urlencoded或from-data格式的参数

获取Post请求参数的常用函数:

  • func (c *Context) PostForm(key string) string
  • func (c *Context) DefaultPostForm(key, defaultValue string) string
  • func (c *Context) GetPostForm(key string) (string, bool)

例子:

func Handler(c *gin.Context) {
    
    
	//获取name参数, 通过PostForm获取的参数值是String类型。
	name := c.PostForm("name")

	// 跟PostForm的区别是可以通过第二个参数设置参数默认值
	name := c.DefaultPostForm("name", "tizi365")

	//获取id参数, 通过GetPostForm获取的参数值也是String类型,
	// 区别是GetPostForm返回两个参数,第一个是参数值,第二个参数是参数是否存在的bool值,可以用来判断参数是否存在。
	id, ok := c.GetPostForm("id")
	if !ok {
    
    
	    // 参数不存在
	}
}

测试Post

1、编辑html页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<form action="http://localhost:8080/form" method="post" action="application/x-www-form-urlencoded">
    用户名:<input type="text" name="username" placeholder="请输入你的用户名">  <br>&nbsp;&nbsp;&nbsp;码:<input type="password" name="userpassword" placeholder="请输入你的密码">  <br>
    <input type="submit" value="提交">
</form>
</body>
</html>

2、编辑post请求

func main() {
    
    
   engine := gin.Default()
   engine.POST("/form", func(c *gin.Context) {
    
    
      types := c.DefaultPostForm("type", "post")
      username := c.PostForm("username")
      password := c.PostForm("userpassword")
      c.String(http.StatusOK, fmt.Sprintf("username:%s ,password: %s,type: %s", username, password, types))
   })
   engine.Run()
}

3、启动服务,服务会一直默认监听8080端口的’/form’

4、复制html的本地绝对路径,在浏览器中打开,提交表单

5、点击提交之后会自动跳转到http://localhost:8080/form,页面显示post获取的表单项,打印显示
在这里插入图片描述

3.3 获取URL路径参数

获取URL路径参数,指的是获取/user/: id这类型路由绑定的参数,这个例子绑定了一个参数id。

获取url路径参数常用函数:

  • func (c *Context) Param(key string) string

例子:

r := gin.Default()
	
r.GET("/user/:id", func(c *gin.Context) {
    
    
	// 获取url参数id
	id := c.Param("id")
})

3.4 将请求参数绑定到struct对象

前面获取参数的方式都是一个个参数的读取,比较麻烦,Gin框架支持将请求参数自动绑定到一个struct对象,这种方式支持Get/Post请求,也支持http请求body内容为json/xml格式的参数。

例子:

下面例子是将请求参数绑定到User struct对象。

// User 结构体定义
type User struct {
    
    
  Name  string `json:"name" form:"name"`
  Email string `json:"email" form:"email"`
}

通过定义struct字段的标签,定义请求参数和struct字段的关系。
下面对User的Name字段的标签进行说明。

struct标签说明:

标签 说明
json:“name” 数据格式为json格式,并且json字段名为name
form:“name” 表单参数名为name
xml:“name”

提示:你可以根据自己的需要选择支持的数据类型,例如需要支持json数据格式,可以这样定义字段标签: json:“name”

下面看下控制器代码:

r.POST("/user/:id", func(c *gin.Context) {
    
    
   // 初始化user struct
   u := User{
    
    }
   // 通过ShouldBind函数,将请求参数绑定到struct对象, 处理json请求代码是一样的。
   // 如果是post请求则根据Content-Type判断,接收的是json数据,还是普通的http请求参数
   if c.ShouldBind(&u) == nil {
    
    
     // 绑定成功, 打印请求参数
     log.Println(u.Name)
     log.Println(u.Email)

    }
    // http 请求返回一个字符串 
    c.String(200, "Success")
})

提示:如果你通过http请求body传递json格式的请求参数,并且通过post请求的方式提交参数,则需要将Content-Type设置为application/json, 如果是xml格式的数据,则设置为application/xml

3.5 Gin如何获取客户ip

r := gin.Default()
	
r.GET("/ip", func(c *gin.Context) {
    
    
	// 获取用户IP
	ip := c.ClientIP()
})

4 Gin处理context响应结果

gin.Context上下文对象支持多种返回处理结果,下面分别介绍不同的响应方式。

4.1 c.String

以字符串方式响应请求,通过String函数返回字符串。

函数定义:

func (c *Context) String(code int, format string, values ...interface{
    
    })

参数说明:

参数 说明
code http状态码
format 返回结果,支持类似Sprintf函数一样的字符串格式定义,例如,%d 代表插入整数,%s代表插入字符串
values 任意个format参数定义的字符串格式参数

例子:

func Handler(c *gin.Context)  {
    
    
	// 例子1:
	c.String(200, "欢迎访问tizi360.com!")
	
	// 例子2: 这里定义了两个字符串参数(两个%s),后面传入的两个字符串参数将会替换对应的%s
	c.String(200,"欢迎访问%s, 你是%s", "tizi360.com!","最靓的仔!")
}

提示: net/http包定义了多种常用的状态码常量,例如:http.StatusOK == 200, http.StatusMovedPermanently == 301, http.StatusNotFound == 404等,具体可以参考net/http包

4.2 c.JSON

以json格式响应请求

// User 定义
type User struct {
    
    
  Name  string `json:"name"` // 通过json标签定义struct字段转换成json字段的名字。
  Email string `json:"email"`
}

// Handler 控制器
func(c *gin.Context) {
    
    
  //初始化user对象
  u := &User{
    
    
    Name:  "tizi365",
    Email: "[email protected]",
  }
  //返回json数据
  //返回结果:{"name":"tizi365", "email":"[email protected]"}
  c.JSON(200, u)
}

4.3 c.XML

以xml格式响应请求

// User 定义, 默认struct的名字就是xml的根节点名字,这里转换成xml后根节点的名字为User.
type User struct {
    
    
  Name  string `xml:"name"` // 通过xml标签定义struct字段转换成xml字段的名字。
  Email string `xml:"email"`
}

// Handler 控制器
func(c *gin.Context) {
    
    
  //初始化user对象
  u := &User{
    
    
    Name:  "tizi365",
    Email: "[email protected]",
  }
  //返回xml数据
  //返回结果:
  //  <?xml version="1.0" encoding="UTF-8"?>
  //  <User><name>tizi365</name><email>[email protected]</email></User>
  c.XML(200, u)
}

4.4 c.File

以文件格式响应请求

下面介绍gin框架如何直接返回一个文件,可以用来做文件下载。

例子1func(c *gin.Context) {
    
    
  //通过File函数,直接返回本地文件,参数为本地文件地址。
  //函数说明:c.File("文件路径")
  c.File("/var/www/1.jpg")
}

例子2func(c *gin.Context) {
    
    
  //通过FileAttachment函数,返回本地文件,类似File函数,区别是可以指定下载的文件名。
  //函数说明: c.FileAttachment("文件路径", "下载的文件名")
  c.FileAttachment("/var/www/1.jpg", "1.jpg")
}

4.5 c.Header

设置http响应头(设置Header)

func(c *gin.Context) {
    
    
  //设置http响应 header, key/value方式,支持设置多个header
  c.Header("site","tizi365")
}

5 Gin中间件

在Gin框架中,中间件(Middleware)指的是可以拦截http请求-响应生命周期的特殊函数,在请求-响应生命周期中可以注册多个中间件,每个中间件执行不同的功能,一个中间执行完再轮到下一个中间件执行。

中间件的常见应用场景如下:

  • 请求限速
  • api接口签名处理
  • 权限校验
  • 统一错误处理

提示:如果你想拦截所有请求做一些事情都可以开发一个中间件函数去实现。

Gin支持设置全局中间件和针对路由分组设置中间件,设置全局中间件意思就是会拦截所有请求,针对分组路由设置中间件,意思就是仅对这个分组下的路由起作用。

5.1 使用中间件

func main() {
    
    
	r := gin.New()

	// 通过use设置全局中间件

	// 设置日志中间件,主要用于打印请求日志
	r.Use(gin.Logger())

	// 设置Recovery中间件,主要用于拦截paic错误,不至于导致进程崩掉
	r.Use(gin.Recovery())

	// 忽略后面代码
}

5.2 自定义中间件

下面通过一个例子,了解如果自定义一个中间件

package main
// 导入gin包
import (
"github.com/gin-gonic/gin"
	"log"
	"time"
)

// 自定义个日志中间件
func Logger() gin.HandlerFunc {
    
    
	return func(c *gin.Context) {
    
    
		t := time.Now()

		// 可以通过上下文对象,设置一些依附在上下文对象里面的键/值数据
		c.Set("example", "12345")

		// 在这里处理请求到达控制器函数之前的逻辑
     
		// 调用下一个中间件,或者控制器处理函数,具体得看注册了多少个中间件。
		c.Next()

		// 在这里可以处理请求返回给用户之前的逻辑
		latency := time.Since(t)
		log.Print(latency)

		// 例如,查询请求状态吗
		status := c.Writer.Status()
		log.Println(status)
	}
}

func main() {
    
    
	r := gin.New()
	// 注册上面自定义的日志中间件
	r.Use(Logger())

	r.GET("/test", func(c *gin.Context) {
    
    
		// 查询我们之前在日志中间件,注入的键值数据
		example := c.MustGet("example").(string)

		// it would print: "12345"
		log.Println(example)
	})

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

6 执行流程图

img

猜你喜欢

转载自blog.csdn.net/qq_42647903/article/details/126121651