Gin简单明了的教程---下

2022-08-23 10:09:39 浏览数 (1)

Gin简单明了的教程---下

  • Gin 中间件
    • 路由中间件
      • ctx.Next()调用该请求的剩余处理程序
      • 一个路由配置多个中间件的执行顺序
      • ctx.Abort()
      • 全局中间件
      • 在路由分组中配置中间件
      • 中间件和对应控制器之间数据共享
      • 中间件注意事项
        • gin默认中间件
        • gin中间件中使用goroutine
      • 处理器链源码分析
  • Gin 文件上传
    • 单文件上传
    • 多文件上传--不同名字的多个文件
    • 多文件上传--相同名字的多个文件
    • 文件上传示例演示
  • Gin 中的 Cookie
    • 设置Cookie
    • 获取Cookie
    • 删除Cookie
    • 演示
    • 多个二级域名共享cookie
  • Gin 中的 Session
    • 基于 Cookie 存储 Session
    • 基于 Redis 存储 Session

Gin 中间件

Gin 框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函 数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、 记录日志、耗时统计等。

通俗的讲:中间件就是匹配路由前和匹配路由完成后执行的一系列操作


路由中间件

Gin 中的中间件必须是一个 gin.HandlerFunc 类型,配置路由的时候可以传递多个 func 回调函数。中间件要放在最后一个回调函数的前面 ,触发的方法都可以称为中间件。

代码语言:javascript复制
package main

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

func InitMiddleWare(c *gin.Context) {
	fmt.Println("init middle ware ")
}

func main() {
	r := gin.Default()
	r.GET("/hello", InitMiddleWare, func(c *gin.Context) {
		c.String(200, "hello world")
	})
	r.Run()
}

可以看到其实我们可以传入不只一个回调处理函数,这些回调函数会组成一个处理器链绑定到当前路由上,而当请求被当前路由拦截时,就会被绑定到当前路由上的拦截器链所处理。

可以类比spring提供的拦截器功能,当然gin框架这里给我们提供了更大的灵活性,因为并没有严格将拦截器和处理请求的处理器区分开来


ctx.Next()调用该请求的剩余处理程序

中间件里面加上 ctx.Next()后,c.Next()的语句后面先不执行,跳转到下一个处理器执行。

可以让我们在路由匹配完成后执行一些操作。比如我们统计一个请求的执行时间

代码语言:javascript复制
func InitMiddleWare(c *gin.Context) {
	fmt.Println("1- init middle ware ")
	start := time.Now().UnixNano()
	// 调用c.Next()请求的剩余处理程序
	// c.Next()的语句后面先不执行,先跳转拦截器链中下一个处理器执行,等到拦截器链执行到完往回返时
	// 才执行c.Next()后面的语句
	c.Next()
	fmt.Println("3-程序执行完成 计算时间")
	end := time.Now().UnixNano()
	fmt.Println(end - start)
}

func ApiRouter(r *gin.Engine) {
	apiRouter := r.Group("/api")
	{
		// 中间件要放在最后一个回调函数的前面
		apiRouter.GET("/", InitMiddleWare, func(ctx *gin.Context) {
			fmt.Println("2 - 中间件")
			ctx.String(200, "/api")
		})
	}
}

这里next提供的功能,可以提供类似于spring mvc中拦截器的preHandle和afterHandle的功效。

代码语言:javascript复制
前置处理
c.next()
后置处理

一个路由配置多个中间件的执行顺序

代码语言:javascript复制
func InitMiddleWareOne(c *gin.Context) {
	fmt.Println("one- init middleware start")
	c.Next()
	fmt.Println("one- init middleware end")
}
func InitMiddleWareTwo(c *gin.Context) {
	fmt.Println("Two- init middleware start")
	c.Next()
	fmt.Println("Two- init middleware end")
}

func ApiRouter(r *gin.Engine) {
	apiRouter := r.Group("/api")
	{
		// 中间件要放在最后一个回调函数的前面
		apiRouter.GET("/", InitMiddleWareOne, InitMiddleWareTwo, func(ctx *gin.Context) {
			fmt.Println("首页")
			ctx.String(200, "/api")
		})
	}
}
  • 控制台输出的结果
代码语言:javascript复制
one- init middleware start
Two- init middleware start
首页
Two- init middleware end
one- init middleware end

ctx.Abort()

Abort是终止的意思,ctx.Abort()表示终止调用处理器链中后续的处理器,当前处理器会执行完毕。

代码语言:javascript复制
package main

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

func InitMiddleWare(c *gin.Context) {
	fmt.Println("preHandle ")
	c.Next()
	fmt.Println("afterHandle ")
}

func main() {
	r := gin.Default()
	// 引入路由模块
	r.GET("/hello", InitMiddleWare, func(c *gin.Context) {
		fmt.Println("pre hello")
		c.Abort()
		fmt.Println("after hello")
		c.String(200, "hello world")
	})
	r.Run()
}

全局中间件

当有一个处理器需要应用到所有路由上时,难道还需要我们一个个挨个去重复绑定吗 ?显然,这个时候通常会有一个全局绑定,即一次全局设置后,会应用到所有的路由上,这里也被称为全局中间件。

代码语言:javascript复制
package main

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

func InitMiddleWareOne(c *gin.Context) {
	fmt.Println("one- init middleware start")
	c.Next()
	fmt.Println("one- init middleware end")
}

func InitMiddleWareTwo(c *gin.Context) {
	fmt.Println("Two- init middleware start")
	c.Next()
	fmt.Println("Two- init middleware end")
}

func main() {
	r := gin.Default()
	r.Use(InitMiddleWareOne,InitMiddleWareTwo)
	// 引入路由模块
	r.GET("/hello", func(c *gin.Context) {
		c.String(200, "hello world")
	})
	r.Run()
}

在路由分组中配置中间件

我们不仅可以全局应用中间件,也可以缩小范围到一个路由组上:

1、为路由组注册中间件有以下两种写法

  • 写法 1:
代码语言:javascript复制
func InitMiddleWareOne(c *gin.Context) {
	fmt.Println("one- init middleware start")
	c.Next()
	fmt.Println("one- init middleware end")
}

func UserRouter(r *gin.Engine) {
	userRouter := r.Group("/user", InitMiddleWareOne)
	{
		u := new(controller.UserController)
		userRouter.GET("/", u.UserGet)
		userRouter.POST("/", u.UserPost)
		userRouter.DELETE("/", u.UserDelete)
		userRouter.PUT("/", u.UserPut)
	}
}
  • 写法 2:
代码语言:javascript复制
func InitMiddleWareOne(c *gin.Context) {
	fmt.Println("one- init middleware start")
	c.Next()
	fmt.Println("one- init middleware end")
}

func UserRouter(r *gin.Engine) {
	userRouter := r.Group("/user")
	userRouter.Use(InitMiddleWareOne)
	{
		u := new(controller.UserController)
		userRouter.GET("/", u.UserGet)
		userRouter.POST("/", u.UserPost)
		userRouter.DELETE("/", u.UserDelete)
		userRouter.PUT("/", u.UserPut)
	}
}

中间件和对应控制器之间数据共享

说白了就是如何在拦截器链执行过程中传递数据,显然在整个拦截器链执行过程中,只有context是一直被传递的,所以如果我们想要在拦截器链执行过程中传递数据,只需要往context中设置数据即可。

代码语言:javascript复制
package main

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

func InitMiddleWareOne(c *gin.Context) {
	c.Set("name", "大忽悠")
}

func main() {
	r := gin.Default()
	r.Use(InitMiddleWareOne)
	r.GET("/hello", func(c *gin.Context) {
		//返回的value是空接口类型,需要先进行类型转换
		val, _ := c.Get("name")
		if name, ok := val.(string); ok {
			c.String(200, "hello %vn", name)
		}
	})
	r.Run()
}

中间件注意事项

gin默认中间件

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

其中:

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

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


gin中间件中使用goroutine

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

代码语言:javascript复制
func LoginMiddleWare(c *gin.Context) {
	fmt.Println("login middle ware")
	c.Set("username", "张三")

	// 定义一个goroutine统计日志
	cCp := c.Copy()
	go func ()  {
		time.Sleep(2 * time.Second)
		// 用了c.Request.URL.Path 也没有问题?
		fmt.Println("Done in path "   cCp.Request.URL.Path)
	}()
}

这一点在官方文档也有说明,可以点击查看

原因:

gin 使用 sync.Pool 重用 context,请求结束会将 context 放回对象池,供其他请求复用。异步任务不 copy context 的话,其他请求可能从对象池中拿到同一个 context,会有问题

对象复用这一点其实很常见,在tomcat底层或者spring底层都有大量对象复用的应用案例。


处理器链源码分析

这里针对处理器的执行过程进行一下简明扼要的源码流程介绍:

  • 程序启动,会去注册相关路由,如: r.Get , r.Post等,而这些方法底层最终都会调用到handle方法
代码语言:javascript复制
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {   
    //计算当前路由真实的拦截请求路径---这里主要会加上当前group对应的请求前缀 
	absolutePath := group.calculateAbsolutePath(relativePath)
	//整合全局处理器,当前group的处理器和应用到当前路由上的处理器,最终整合为一个处理器链后返回
	handlers = group.combineHandlers(handlers)
	//注册路由
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}
  • 当请求被某个路由拦截时
代码语言:javascript复制
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
		//从这个tree中寻找到处理当前请求的router
		value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
		if value.params != nil {
			c.Params = *value.params
		}
		//执行当前路由绑定的处理器链
		if value.handlers != nil {
			c.handlers = value.handlers
			c.fullPath = value.fullPath
			//调用处理器的入口处
			c.Next()
			c.writermem.WriteHeaderNow()
			return
		}
		....
		break
	}
	...
}
  • 拦截器调用的入口
代码语言:javascript复制
func (c *Context) Next() {
	c.index  
	//每调用一次Next索引加一
	for c.index < int8(len(c.handlers)) {
		//相关处理器回调函数会放在一个数组中,通过索引定位到具体的处理器函数,然后执行调用
		c.handlers[c.index](c)
		c.index  
	}
}
  • abort就是设置当前索引为整数最大值
代码语言:javascript复制
func (c *Context) Abort() {
	c.index = abortIndex
}

Gin 文件上传

注意:需要在上传文件的 form 表单上面需要加入 enctype=“multipart/form-data”

单文件上传

官方示例

代码语言:javascript复制
package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
	"path"
)

func main() {
	router := gin.Default()
	// 为 multipart forms 设置较低的内存限制 (默认是 32 MiB)
	router.MaxMultipartMemory = 8 << 20 // 8 MiB

	router.POST("/upload", func(c *gin.Context) {
		// 单文件
		file, _ := c.FormFile("file")
		log.Println(file.Filename)

		dst := path.Join("./", file.Filename)

		// 上传文件至指定的完整文件路径
		c.SaveUploadedFile(file, dst)

		c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
	})
	router.Run(":8080")
}

多文件上传–不同名字的多个文件

代码语言:javascript复制
package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
	"path"
)

func main() {
	router := gin.Default()
	// 为 multipart forms 设置较低的内存限制 (默认是 32 MiB)
	router.MaxMultipartMemory = 8 << 20 // 8 MiB

	router.POST("/upload", func(c *gin.Context) {
		file1, _ := c.FormFile("file1")
		file2, _ := c.FormFile("file2")

		dst := path.Join("./", file1.Filename)
		c.SaveUploadedFile(file1, dst)

		dst = path.Join("./", file2.Filename)
		c.SaveUploadedFile(file2, dst)

		c.String(http.StatusOK, "success !")
	})
	router.Run(":8080")
}

多文件上传–相同名字的多个文件

代码语言:javascript复制
package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
	"path"
)

func main() {
	router := gin.Default()
	// 为 multipart forms 设置较低的内存限制 (默认是 32 MiB)
	router.MaxMultipartMemory = 8 << 20 // 8 MiB

	router.POST("/upload", func(c *gin.Context) {
		// Multipart form
		form, _ := c.MultipartForm()
		files := form.File["upload[]"]

		for _, file := range files {
			log.Println(file.Filename)

			dst := path.Join("./", file.Filename)

			// 上传文件至指定目录
			c.SaveUploadedFile(file, dst)
		}
		c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
	})

	router.Run(":8080")
}

文件上传示例演示

请提供一个可以上传图片的接口,要求图片按照天数分割到不同的文件夹下存储,并且提供一个查询接口,可以查询某一天上传的图片有哪些。

代码语言:javascript复制
package controller

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"os"
	"path"
	"strconv"
	"time"
)

type ImgController struct {
	//图片允许的后缀名
	allowExtMap map[string]bool
}

func (img *ImgController) AllowExtMap() map[string]bool {
	return img.allowExtMap
}

func (img *ImgController) uploadImg(c *gin.Context) {
	imgFile, err := c.FormFile("img")
	if err != nil {
		c.Error(err)
		return
	}
	//获取上传图片文件的后缀名
	extName := path.Ext(imgFile.Filename)
	//校验
	if _, ok := img.AllowExtMap()[extName]; !ok {
		c.String(401, "文件格式不合法")
		return
	}
	//创建图片保存目录
	now := time.Now()
	year := now.Year()
	month := now.Month()
	day := now.Day()
	hour := now.Hour()
	minute := now.Minute()
	join := "./"   strconv.Itoa(year)   "-"   month.String()   "-"   strconv.Itoa(day)   "-"   strconv.Itoa(hour)   "-"   strconv.Itoa(minute)
	if err := os.MkdirAll(join, 0666); err != nil {
		fmt.Println(err)
	}
	join = path.Join(join, imgFile.Filename)
	fmt.Printf("上传图片保存路径为: %vn", join)
	//上传文件
	c.SaveUploadedFile(imgFile, join)
}

func (img *ImgController) getImg(c *gin.Context) {

}

func RegisterImgRouter(c *gin.Engine) {
	imgController := &ImgController{
		allowExtMap: map[string]bool{
			".jpg":  true,
			".png":  true,
			".gif":  true,
			".jpeg": true,
		},
	}
	imgGroup := c.Group("/img")
	imgGroup.POST("/", imgController.uploadImg)
	imgGroup.GET("/", imgController.uploadImg)
}
代码语言:javascript复制
package main

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

func main() {
	engine := gin.Default()
	registerRouter(engine)
	engine.Run()
}

func registerRouter(engine *gin.Engine) {
	controller.RegisterImgRouter(engine)
}

查询接口没有实现,感兴趣大家可自行实现,并且建议将上传文件保存的相关逻辑放到Dao层完成


Gin 中的 Cookie

  • HTTP 是无状态协议。简单地说,当你浏览了一个页面,然后转到同一个网站的另一个页面,服务器无法认识到这是同一个浏览器在访问同一个网站。每一次的访问,都是没有任何关系的。如果我们要实现多个页面之间共享数据的话我们就可以使用Cookie 或者Session 实 现
  • cookie 是存储于访问者计算机的浏览器中。可以让我们用同一个浏览器访问同一个域名 的时候共享数据

Cookie 能实现的功能

  • 保持用户登录状态
  • 保存用户浏览的历史记录
  • 猜你喜欢,智能推荐
  • 电商网站的加入购物车

前面我们已经使用过ctx.Set(“username”) 和 ctx.Get(“username”)来进行数据的保存和共享,但这个使用的只针对是单页面的数据共享,要想实现多页面的共享,就需要Cookie或者Session。


设置Cookie

c.SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)其中:

  • 第一个参数key
  • 第二个参数value
  • 第三个参数过期时间.如果只想设置Cookie 的保存路径而不想设置存活时间,可以在第三个参数中传递nil
  • 第四个参数cookie 的路径
  • 第五个参数cookie 的路径Domain 作用域本地调试配置成localhost , 正式上线配置成域名
  • 第六个参数是secure ,当secure 值为true 时,cookie 在HTTP 中是无效,在HTTPS 中才有效
  • 第七个参数httpOnly,是微软对COOKIE 做的扩展。如果在COOKIE 中设置了“httpOnly”属性,则通过程序(JS 脚本、applet 等)将无法读取到COOKIE 信息,防止XSS 攻击产生

获取Cookie

代码语言:javascript复制
cookie, err := c.Cookie("name")

删除Cookie

代码语言:javascript复制
把第三个参数时间,即过期时间设置为-1

演示

代码语言:javascript复制
func ApiRouter(r *gin.Engine) {
	apiRouter := r.Group("/api")
	{
		apiRouter.GET("/", func(ctx *gin.Context) {
			// 设置Cookie
			ctx.SetCookie("username", "张三", 3600, "/", "localhost", false, false)
			fmt.Println("首页")
			ctx.String(200, "/api")
		})

		apiRouter.GET("/news", func(ctx *gin.Context) {
			// 获取Cookie
			username, _ := ctx.Cookie("username")
			fmt.Println(username)
			ctx.String(200, "/news/" username)
		})
	}
}

多个二级域名共享cookie

我们想的是用户在a.test.com 中设置Cookie 信息后在b.test.com 中获取刚才设置的cookie,也就是实现多个二级域名共享cookie, 这时候的话我们就可以这样设置cookie。

代码语言:javascript复制
c.SetCookie("usrename", "张三", 3600, "/", ".test.com", false, true)

Gin 中的 Session

session 是另一种记录客户状态的机制,不同的是 Cookie 保存在客户端浏览器中,而 session 保存在服务器上。

Session 的工作流程:

当客户端浏览器第一次访问服务器并发送请求时,服务器端会创建一个 session 对象,生成 一个类似于 key,value 的键值对,然后将 value 保存到服务器 将 key(cookie)返回到浏览器(客 户)端。

浏览器下次访问时会携带 key(cookie),找到对应的 session(value)。

Gin 中使用 Session Gin 官方没有给我们提供 Session 相关的文档,这个时候我们可以使用第三方的 Session 中间 件来实现.

https://github.com/gin-contrib/sessions

gin-contrib/sessions中间件支持的存储引擎:

• cookie • memstore • redis • memcached • mongodb


基于 Cookie 存储 Session

1、安装 session 包

代码语言:javascript复制
go get github.com/gin-contrib/sessions

2、基本的 session 用法

代码语言:javascript复制
package main

import (
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/cookie"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	//创建基于cookie的存储引擎,参数是用于加密的秘钥
	store := cookie.NewStore([]byte("secret111"))
	//设置session中间件,参数mysession,指的是session的名字,也是cookie的名字
	r.Use(sessions.Sessions("mysession", store))
	r.GET("/", func(c *gin.Context) {
		//初始化session对象
		session := sessions.Default(c)
		//设置过期时间
		session.Options(sessions.Options{
			MaxAge: 3600 * 6, //默认过期时间为30天
		})
		//设置Session
		session.Set("username", "dhy")
		session.Save()
		c.String(200, "success")
	})
	r.POST("/", func(c *gin.Context) {
		//初始化session对象
		session := sessions.Default(c)
		//通过session.Get读取session值
		username := session.Get("username")
		c.String(200, username.(string))
	})
	r.Run()
}

基于 Redis 存储 Session

如果我们想将 session 数据保存到 redis 中,只要将 session 的存储引擎改成 redis 即可。

使用 redis 作为存储引擎的例子:

  • 首先安装 redis 存储引擎的包
代码语言:javascript复制
go get github.com/gin-contrib/sessions/redis

例子:

代码语言:javascript复制
package main

import (
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/redis"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	// 初始化基于 redis 的存储引擎 
	//参数说明: 
	//第 1 个参数 - redis 最大的空闲连接数 
	//第 2 个参数 - 数通信协议 tcp 或者 udp 
	//第 3 个参数 - redis 地址, 格式,host:port 
	//第 4 个参数 - redis 密码 
	//第 5 个参数 - session 加密密钥
	store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret111"))

	//设置session中间件,参数mysession,指的是session的名字,也是cookie的名字
	r.Use(sessions.Sessions("mysession", store))
	r.GET("/", func(c *gin.Context) {
		//初始化session对象
		session := sessions.Default(c)
		//设置过期时间
		session.Options(sessions.Options{
			MaxAge: 3600 * 6, //默认过期时间为30天
		})
		//设置Session
		session.Set("username", "dhy")
		session.Save()
		c.String(200, "success")
	})
	r.POST("/", func(c *gin.Context) {
		//初始化session对象
		session := sessions.Default(c)
		//通过session.Get读取session值
		username := session.Get("username")
		c.String(200, username.(string))
	})
	r.Run()
}

0 人点赞