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

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Gin简单明了的教程---下相关的知识,希望对你有一定的参考价值。

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


Gin 中间件

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

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


路由中间件

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

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()的语句后面先不执行,跳转到下一个处理器执行。

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

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的功效。

前置处理
c.next()
后置处理

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

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")
		)
	

  • 控制台输出的结果
one- init middleware start
Two- init middleware start
首页
Two- init middleware end
one- init middleware end

ctx.Abort()

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

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()


全局中间件

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

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:
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:
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中设置数据即可。

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 %v\\n", 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())

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方法
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()

  • 当请求被某个路由拦截时
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
	
	...

  • 拦截器调用的入口
func (c *Context) Next() 
	c.index++
	//每调用一次Next索引加一
	for c.index < int8(len(c.handlers)) 
		//相关处理器回调函数会放在一个数组中,通过索引定位到具体的处理器函数,然后执行调用
		c.handlers[c.index](c)
		c.index++
	

  • abort就是设置当前索引为整数最大值
func (c *Context) Abort() 
	c.index = abortIndex


Gin 文件上传

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

单文件上传

官方示例

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")


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

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")



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

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")


文件上传示例演示

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

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以上是关于Gin简单明了的教程---下的主要内容,如果未能解决你的问题,请参考以下文章

一文了解​chia奇亚矿机黑马哥挖矿教程简单明了

gin-jwt对API进行权限控制

在 Python 中使用 C++ DLL

go-gin 框架的多级分组

gin框架学习-Gin框架和Gorm框架搭建一个简单的API微服务

php中的抽象方法和抽象类,简单明了,一点通