Gin 是一个用 Go 语言编写的高性能 Web 框架,它设计得非常灵活且高效,其中一个核心特性就是中间件(Middleware)的支持。中间件是一种可以在 HTTP 请求处理流程中的特定点插入自定义处理逻辑的功能,这对于实现诸如身份验证、日志记录、错误处理、性能监控等横切关注点非常有用。
Gin 中间件基础
1、类型定义
Gin 中的中间件必须是一个 gin.HandlerFunc
类型,即一个接受 *gin.Context
参数且无返回值的函数。你可以直接定义这种类型的函数,或者定义一个返回 gin.HandlerFunc
的工厂函数。
func myMiddleware(c *gin.Context) {
// 在此处执行中间件逻辑
c.Next() // 调用 Next() 继续执行后续的处理器或中间件
// 中间件逻辑也可以在这之后执行,如响应后处理
}
2、使用中间件
使用 r.Use()
方法将中间件添加到路由器中。全局中间件会影响所有路由,而通过路由组添加的中间件仅影响该组内的路由。
r := gin.Default()
r.Use(myMiddleware) // 全局中间件
authGroup := r.Group("/auth")
authGroup.Use(authenticationMiddleware) // 分组中间件
(1)、控制流程
c.Next()
用于传递控制权给链中的下一个处理器或中间件。一旦调用,控制流将继续直至最后一个处理器,并最终返回到调用 c.Next()
后面的代码。在Go语言的Gin框架中,c.Next()
是一个关键函数,用于控制请求处理流程中的中间件和路由处理器的执行顺序。
Gin中间件是一种预处理函数,它在实际处理HTTP请求之前或之后执行一些操作,比如验证、日志记录、响应修改等。每个中间件都有机会通过调用c.Next()
来决定是否继续执行下一个中间件或最终的路由处理器。
(2)、作用
当在中间件中调用c.Next()
时,它会告诉Gin框架继续执行链中的下一个中间件或路由处理器。如果当前中间件后面还有其他中间件,则这些中间件会依次被执行,直到到达路由处理函数。执行完路由处理函数后,控制权会逆序返回到调用过c.Next()
的中间件中,执行它们的后续逻辑(如果有)。
(3)、执行流程
- Gin从第一个注册的中间件开始执行。
- 当前中间件执行到
c.Next()
时,控制权传递给下一个中间件或路由处理器,但当前中间件的执行并不会在此停止,而是暂停,等待所有后续中间件及路由处理器执行完毕。 - 所有中间件和路由处理器执行完毕后,控制权逆序返回到之前调用过
c.Next()
的每个中间件,允许它们执行c.Next()
之后的代码(即所谓的“后处理”逻辑)。
(4)、注意事项
-
如果在某个中间件中确定不应该继续处理请求(例如认证失败),应使用
c.Abort()
来中止请求处理流程,之后的中间件和路由处理器将不会被执行。调用c.Abort()
后,通常还会伴随一个状态码设置操作,如c.AbortWithStatus(http.StatusUnauthorized)
。 -
调用
c.Next()
后,即使路由处理器已经执行完毕,当前中间件的剩余部分仍然会被执行,这为中间件提供了在请求处理前后执行特定逻辑的能力。 -
在使用
c.Next()
时,需要注意并发安全问题,特别是当在中间件中启动新的Goroutine时,确保对共享资源的访问是同步的。
示例代码1
先执行全局中间件>>处理函数>>中间件c.Next()之后的业务
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func middleware1(c *gin.Context) {
// 在调用c.Next()之前的操作
fmt.Println("Executing Middleware 1")
c.Next() // 继续执行下一个中间件或路由处理器
// c.Next()之后的操作,如日志记录等
fmt.Println("Finished Middleware 1")
}
func handler(c *gin.Context) {
fmt.Println("Executing Handler")
c.String(200, "Hello, World!")
}
func main() {
r := gin.Default()
r.Use(middleware1) // 注册中间件(全局中间件)
r.GET("/test", handler)
r.Run(":8080")
}
/*输出结果
Executing Middleware 1
Executing Handler
Finished Middleware 1
[GIN] 2024/04/29 - 10:41:52 | 200 | 1.6439ms | 127.0.0.1 | GET "/test"
*/
示例代码2
先执行全局中间件>>处理函数>>中间件c.Next()之后的业务(下一个中间件如果有的话)>>中间件c.Next()之后的业务>>返回到上一中间件
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func middleware1(c *gin.Context) {
// 在调用c.Next()之前的操作
fmt.Println("Executing Middleware 1")
c.Next() // 继续执行下一个中间件或路由处理器
// c.Next()之后的操作,如日志记录等
fmt.Println("Finished Middleware 1")
}
func middleware2(c *gin.Context) {
// 在调用c.Next()之前的操作
fmt.Println("Executing Middleware 2")
c.Next() // 继续执行下一个中间件或路由处理器
// c.Next()之后的操作,如日志记录等
fmt.Println("Finished Middleware 2")
}
func handler(c *gin.Context) {
fmt.Println("Executing Handler")
c.String(200, "Hello, World!")
}
func main() {
r := gin.Default()
r.Use(middleware1) // 注册中间件
r.GET("/test", handler, middleware2)
r.Run(":8080")
}
/*输出结果
Executing Middleware 1
Executing Handler
Executing Middleware 2
Finished Middleware 2
Finished Middleware 1
[GIN] 2024/04/29 - 10:59:17 | 200 | 1.7713ms | 127.0.0.1 | GET "/test"
*/
示例代码3
执行流程如打印结果所示
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func middleware1(c *gin.Context) {
// 在调用c.Next()之前的操作
fmt.Println("Executing Middleware 1")
c.Next() // 继续执行下一个中间件或路由处理器
// c.Next()之后的操作,如日志记录等
fmt.Println("Finished Middleware 1")
}
func middleware2(c *gin.Context) {
// 在调用c.Next()之前的操作
fmt.Println("Executing Middleware 2")
c.Next() // 继续执行下一个中间件或路由处理器
// c.Next()之后的操作,如日志记录等
fmt.Println("Finished Middleware 2")
}
func middleware3(c *gin.Context) {
// 在调用c.Next()之前的操作
fmt.Println("Executing Middleware 3")
c.Next() // 继续执行下一个中间件或路由处理器
// c.Next()之后的操作,如日志记录等
fmt.Println("Finished Middleware 3")
}
func handler(c *gin.Context) {
fmt.Println("Executing Handler")
c.String(200, "Hello, World!")
}
func main() {
r := gin.Default()
r.Use(middleware1) // 注册中间件
r.GET("/test", middleware3, handler, middleware2)
r.Run(":8080")
}
/*输出结果
Executing Middleware 1
Executing Middleware 3
Executing Handler
Executing Middleware 2
Finished Middleware 2
Finished Middleware 3
Finished Middleware 1
[GIN] 2024/04/29 - 11:03:01 | 200 | 5.2126ms | 127.0.0.1 | GET "/test"
*/
3、c.Next()
如果你有一个简单的日志记录中间件但忘记调用c.Next()
:
func loggingMiddleware(c *gin.Context) {
log.Printf("Executing loggingMiddleware")
// 忘记调用c.Next()
log.Printf("Finished loggingMiddleware")
}
c.Next() 是将请求传递给请求链中下一个处理方法执行
c.Abort() 当处理出现错误时将提前结束请求处理
两个方法会改变中间件的处理逻辑顺序及中间件的嵌套,如果中间件中都没有使用c.Next(),则中间件按注册顺序依次执行,如果在中间中调用了Next(),则在当前中间件调用Next()处执行下一个中间件,其处理顺序及嵌套情况如下:
//中间件中调用了Next()
func m1(c *gin.Context) {
fmt.Println("m1 in")
c.Next()
fmt.Println("m1 out")
}
func m2(c *gin.Context) {
fmt.Println("m2 in")
c.Next()
fmt.Println("m2 out")
}
func index() gin.HandlerFunc {
fmt.Println("index")
return func(c *gin.Context) {
fmt.Println("index start")
c.Next()
fmt.Println("index end")
}
}
//中间件中没有调用Next()
func m1(c *gin.Context) {
fmt.Println("m1 in")
//c.Next()
fmt.Println("m1 out")
}
func m2(c *gin.Context) {
fmt.Println("m2 in")
//c.Next()
fmt.Println("m2 out")
}
func index() gin.HandlerFunc {
fmt.Println("index")
return func(c *gin.Context) {
fmt.Println("index start")
//c.Next()
fmt.Println("index end")
}
}
func main() {
r := gin.Default()
r.GET("/test", m1, m2, index)
r.Run(":8080")
}
(1)、中间件中调用了Next()则执行顺序及嵌套关系如下图:
(2)、中间件中没有调用Next()则执行顺序就是 m1-m2-index
(3)、有跟无c.Next()嵌套执行关系
4、Abort()
c.Abort()
可以用来立即停止执行当前请求的剩余中间件和处理器,常用于提前结束请求处理,比如权限验证失败时。
当调用 c.Abort()
后,Gin框架将不会继续调用 c.Next()
后的中间件或者路由处理函数,而是直接进入已执行过的中间件的“后处理”逻辑(如果有的话)。这对于需要在拒绝请求后执行一些清理工作或记录日志的场景特别有帮助。
通常,c.Abort()
会与设置响应状态码的方法一起使用,以明确告知客户端请求已被拒绝的原因,例如:
func authMiddleware(c *gin.Context) {
// 检查认证信息,假设这里检查失败
if !isAuthenticated(c.Request) {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
// 认证成功,继续执行下一个中间件或路由处理器
c.Next()
}
在这个例子中,如果用户未通过认证,authMiddleware
会使用 c.AbortWithStatusJSON()
直接终止请求,并返回401 Unauthorized状态码及错误信息,之后的中间件和路由处理器就不会再执行了。
注意:当调用 c.Abort()
后,Gin框架将不会继续调用 c.Next()
后的中间件或者路由处理函数,而不是停止执行该中间件里的代码,以上代码c.AbortWithStatusJSON()
内部会调用c.Abort(),
因此,在权限检查失败后,通过return
直接结束当前中间件的执行,可以避免不必要的流程继续,同时保持代码的清晰和逻辑的准确性
例子:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func middleware1(c *gin.Context) {
// 在调用c.Next()之前的操作
fmt.Println("Executing Middleware 1")
c.Next() // 继续执行下一个中间件或路由处理器
// c.Next()之后的操作,如日志记录等
fmt.Println("Finished Middleware 1")
}
func middleware2(c *gin.Context) {
// 在调用c.Next()之前的操作
fmt.Println("Executing Middleware 2")
c.Next() // 继续执行下一个中间件或路由处理器
// c.Next()之后的操作,如日志记录等
fmt.Println("Finished Middleware 2")
}
func middleware3(c *gin.Context) {
// 在调用c.Next()之前的操作
fmt.Println("Executing Middleware 3")
c.Next() // 继续执行下一个中间件或路由处理器
// c.Next()之后的操作,如日志记录等
fmt.Println("Finished Middleware 3")
}
func errMiddleware(c *gin.Context) {
fmt.Println("Executing errMiddleware")
if !isAuthenticated(c.Request) {
//根据具体业务,正常情况下需要调用c.Abort()来达到停止调用c.Next()后的中间件或者路由处理函数
//c.Abort()
//c.AbortWithStatusJSON()内部会调用c.Abort(),这会导致后续的中间件和路由处理器不再被执行
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
//使用return是因为不必执行其他代码了,如果不返回会继续执行fmt.Println("Finished errMiddleware")代码
return
}
c.Next() // 继续执行下一个中间件或路由处理器
fmt.Println("Finished errMiddleware")
}
func isAuthenticated(request *http.Request) bool {
// 假设验证失败返回false
return false
}
func handler(c *gin.Context) {
fmt.Println("Executing Handler")
c.String(200, "Hello, World!")
}
func main() {
r := gin.Default()
r.Use(middleware1) // 注册中间件
r.GET("/test", middleware3, handler, errMiddleware, middleware2)
r.Run(":8080")
}
/*输出结果
Executing Middleware 1
Executing Middleware 3
Executing Handler
Executing errMiddleware
[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 200 with 401
Finished Middleware 3
Finished Middleware 1
[GIN] 2024/04/29 - 14:52:51 | 200 | 1.9487ms | 127.0.0.1 | GET "/test"
*/
5、Handler 间的通信
通过 gin.Context
可以在中间件和处理函数之间传递数据,或者使用 engine.Set()
和 engine.Get()
存储和检索请求级别的上下文数据。
6、洋葱模型
Gin 的中间件执行模式类似于洋葱模型,请求从外层中间件进入,逐步向内执行,然后逐层返回,形成一种嵌套的调用结构,这使得每个中间件都能对请求和响应过程产生影响。
7、 应用场景
- 权限验证:检查请求头或 cookie 中的令牌来验证用户身份。
- 日志记录:记录请求开始和结束时间,以及请求的相关信息。
- 请求限速:限制每个客户端的请求频率,防止恶意攻击或资源滥用。
- API 签名处理:验证请求的签名是否正确,确保数据完整性。
- 统一错误处理:捕获并统一处理程序中的错误,提供标准错误响应。
8、中间件和对应控制器之间共享数据
(1).设置值
c.Set("username","张三")
(2).获取值
username, _ := c.Get("username")
(3).完整代码
中间件设置值
package middlewares
import (
"fmt"
"github.com/gin-gonic/gin"
"time"
)
func InitMiddleware(c *gin.Context) {
//判断用户是否登录
fmt.Println(time.Now())
fmt.Println(c.Request.URL)
//设置值, 和对应控制器之间共享数据
c.Set("username", "sph")
}
控制器获取值
package login
import (
"fmt"
"github.com/gin-gonic/gin"
)
type IndexController struct {
}
func (con IndexController) Index(c *gin.Context) {
//获取中间件中设置的username值,数据共享
username, _ := c.Get("username")
fmt.Println(username)
//username是一个空接口类型,故要使用则需要用类型断言转换username
v, ok := username.(string)
if ok != true {
c.String(200, "获取值失败")
} else {
c.String(200, "获取的值:"+v)
}
}
使用
package main
import (
"gin-mall/controllers/login"
"gin-mall/middlewares"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.Use(middlewares.InitMiddleware)
r.GET("/test", login.IndexController{}.Index)
r.Run(":8080")
}
9、中间件注意事项
顺序问题:中间件的执行顺序遵循它们被注册的顺序。使用r.Use()添加的全局中间件会先于路由级别中间件执行,而路由组内的中间件则按照定义顺序执行。确保你理解并合理安排中间件的调用顺序,以满足逻辑需求。
调用c.Next():确保在需要继续执行后续中间件或路由处理器时调用c.Next()。如果忘记调用,会导致请求处理流程中断,后续中间件及处理器不会被执行。
错误处理:使用c.Abort()或c.AbortWithStatus()来立即终止请求处理并返回错误响应。如果中间件中检测到错误,应使用这些方法,并在必要时提供合适的错误信息和状态码。
并发安全:在使用Goroutines时,要特别注意gin.Context对象的使用。由于gin.Context并非线程安全,如果在中间件中启动新的Goroutine处理请求,应当复制必要的上下文数据,而非直接传递gin.Context。
资源管理:确保中间件在执行前后正确地打开和关闭资源,比如数据库连接、文件句柄等,避免资源泄露。
性能考量:中间件可能会影响整体性能,特别是那些执行耗时操作(如数据库查询、网络请求)的中间件。尽量优化中间件逻辑,减少不必要的计算和延迟。
日志和监控:利用中间件记录请求日志和性能指标,可以帮助你更好地理解和优化应用性能,但要平衡好日志的详细程度与存储开销。
测试:为中间件编写单元测试和集成测试,确保它们在各种情况下都能正确工作,特别是在复杂的请求处理流程中。
避免过度使用:虽然中间件提供了解耦和重用代码的有效方式,但过度使用会导致代码结构复杂,难以追踪和调试。确保每个中间件都有明确的职责范围。
文档和注释:清晰地记录每个中间件的功能、输入输出以及使用场景,对于团队协作和未来的维护至关重要。
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()
package middlewares import ( "fmt" "github.com/gin-gonic/gin" "time" ) func InitMiddleware(c *gin.Context) { //判断用户是否登录 fmt.Println(time.Now()) fmt.Println(c.Request.URL) //设置值, 和对应控制器之间共享数据 c.Set("username", "sph") //gin 中间件中使用 goroutine //当在中间件或 handler 中启动新的 goroutine 时,不能使用原始的上下文(c *gin.Context), //必须使用其只读副本(c.Copy()) /* 在使用Goroutines时,要特别注意gin.Context对象的使用。 由于gin.Context并非线程安全,如果在中间件中启动新的Goroutine处理请求, 应当复制必要的上下文数据,而非直接传递gin.Context */ cCp := c.Copy() go func() { time.Sleep(2 * time.Second) fmt.Println("gin 中间件中使用 goroutine" + cCp.Request.URL.Path) }() }
)