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()后,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
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、为路由组注册中间件有以下两种写法
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)
}
}
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.Default()默认使用了Logger和Recovery中间件,
其中:
如果不想使用上面的默认中间件,可以使用gin.New()新建一个没有任何中间件的路由
当在中间件或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底层都有大量对象复用的应用案例。
这里针对处理器的执行过程进行一下简明扼要的源码流程介绍:
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++
}
}
func (c *Context) Abort() {
c.index = abortIndex
}
注意:需要在上传文件的 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(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("上传图片保存路径为: %v\n", 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)
}
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层完成
同一个域名 的时候共享数据
Cookie 能实现的功能
前面我们已经使用过ctx.Set(“username”) 和 ctx.Get(“username”)来进行数据的保存和共享,但这个使用的只针对是单页面的数据共享,要想实现多页面的共享,就需要Cookie或者Session。
c.SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
其中:
cookie, err := c.Cookie("name")
把第三个参数时间,即过期时间设置为-1
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)
})
}
}
我们想的是用户在a.test.com 中设置Cookie 信息后在b.test.com 中获取刚才设置的cookie,也就是实现多个二级域名共享cookie, 这时候的话我们就可以这样设置cookie。
c.SetCookie("usrename", "张三", 3600, "/", ".test.com", false, true)
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
1、安装 session 包
go get github.com/gin-contrib/sessions
2、基本的 session 用法
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()
}
如果我们想将 session 数据保存到 redis 中,只要将 session 的存储引擎改成 redis 即可。
使用 redis 作为存储引擎的例子:
go get github.com/gin-contrib/sessions/redis
例子:
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()
}