Gin框架中如何快速掌握中间件的使用技巧?
摘要:Gin 中间件 定义中间件 中间件必须是一个gin.HandlerFunc类型。 package main import ( "fmt" "github.com
Gin 中间件
定义中间件
中间件必须是一个gin.HandlerFunc类型。
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"time"
)
// 计时中间件
func Timer1() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next() // 调用后续中间件和路由处理
//c.Abort() // 中断后续中间件和路由处理,返回给客户端。相当于return了
end := time.Now()
fmt.Println("耗时1:", end.Sub(start))
}
}
// 计时中间件
func Timer2() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
end := time.Now()
fmt.Println("耗时2:", end.Sub(start))
}
}
func main() {
r := gin.Default()
r.Use(Timer1(), Timer2()) // 注册中间件
r.GET("/", func(c *gin.Context) {
time.Sleep(3 * time.Second)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
})
})
r.Run(":8080")
}
注册中间件
全局注册 中间件
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"time"
)
// 计时中间件
func Timer1() gin.HandlerFunc {
return func(c *gin.Context) {
println("注册顺序1")
start := time.Now()
c.Next() // 调用后续中间件和路由处理
//c.Abort() // 中断后续中间件和路由处理,返回给客户端。相当于return了
end := time.Now()
fmt.Println("响应顺序1:", end.Sub(start))
}
}
func main() {
r := gin.Default()
r.Use(Timer1()) // 注册全局中间件
r.GET("/", func(c *gin.Context) {
time.Sleep(3 * time.Second)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
})
})
r.Run(":8080")
}
// 中间件注册结果
/*
注册顺序1
响应顺序1
*/
为路由组 添加中间件
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"time"
)
func Timer() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
end := time.Now()
fmt.Println("耗时:", end.Sub(start))
}
}
func main() {
// 创建一个默认的路由引擎
r := gin.Default()
// 创建一个路由组,路径为 "/test"
testRouter := r.Group("/test")
// 路由组添加中间件
testRouter.Use(Timer())
// 路由组添加路由
testRouter.GET("/", func(c *gin.Context) {
time.Sleep(3 * time.Second)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
})
})
// 启动 HTTP 服务器,默认监听在 0.0.0.0:8080
r.Run() // 等价于 r.Run(":8080")
}
为单个路由 添加中间件
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"time"
)
func Timer02() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
end := time.Now()
fmt.Println("耗时:", end.Sub(start))
}
}
func main() {
// 创建一个默认的路由引擎
r := gin.Default()
// 给一个【普通路由】添加中间件
r.GET("/", Timer02(), func(c *gin.Context) {
time.Sleep(3 * time.Second)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
})
})
// 启动 HTTP 服务器,默认监听在 0.0.0.0:8080
r.Run() // 等价于 r.Run(":8080")
}
注册多个中间件执行顺序
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"time"
)
// 计时中间件
func Timer1() gin.HandlerFunc {
return func(c *gin.Context) {
println("注册顺序1")
start := time.Now()
c.Next() // 调用后续中间件和路由处理
//c.Abort() // 中断后续中间件和路由处理,返回给客户端。相当于return了
end := time.Now()
fmt.Println("响应顺序1:", end.Sub(start))
}
}
// 计时中间件
func Timer2() gin.HandlerFunc {
return func(c *gin.Context) {
println("注册顺序2")
start := time.Now()
c.Next()
end := time.Now()
fmt.Println("响应顺序2:", end.Sub(start))
}
}
// 计时中间件
func Timer3() gin.HandlerFunc {
return func(c *gin.Context) {
println("注册顺序3")
start := time.Now()
c.Next()
end := time.Now()
fmt.Println("响应顺序3:", end.Sub(start))
}
}
func main() {
r := gin.Default()
r.Use(Timer1(), Timer2()) // 注册中间件
r.Use(Timer3())
r.GET("/", func(c *gin.Context) {
time.Sleep(3 * time.Second)
c.JSON(200, gin.H{
"code": 200,
"msg": "ok",
})
})
r.Run(":8080")
}
// 中间件注册结果
/*
注册顺序1
注册顺序2
注册顺序3
响应顺序3: 83ns
响应顺序2: 27.334µs
响应顺序1: 38.542µs
*/
中间件使用事项
Gin的默认中间件
package main
import "github.com/gin-gonic/gin"
func main() {
// 创建一个默认的路由引擎
r := gin.Default()
// 默认中间件:Logger:日志中间件, Recovery:恢复中间件
r.Use(gin.Logger(), gin.Recovery())
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run(":8080")
}
中间件中使用 goroutine的问题
Gin上下文的生命周期问题
Gin的上下文(*gin.Context)是请求级别的,这意味着:
每个HTTP请求都会创建一个新的上下文实例
当请求处理完成后,上下文会被回收和重用(通过sync.Pool)
如果在goroutine中直接使用原始上下文,当主请求处理完成时,上下文可能已经被回收
竞态条件(Race Condition)
当你在goroutine中直接使用原始上下文时,可能会发生:
主goroutine完成请求处理,释放上下文
异步goroutine还在访问上下文的数据
导致数据竞争或访问已释放的内存
c.Copy() 的作用
c.Copy() 方法创建上下文的只读副本,具有以下特点:
// c.Copy() 返回的副本包含:
- 请求信息(Request)的副本
- 响应信息(Writer)的副本(但写入操作会被忽略)
- 参数(Params)的副本
- 处理器链(Handlers)的副本
- 但**不包含**连接池等共享资源
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"time"
)
// 深度解读:Gin中间件中使用goroutine的问题
// WrongGoroutineHandler 1. 错误的示例:在goroutine中直接使用原始上下文
func WrongGoroutineHandler() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("=== 错误示例开始 ===")
fmt.Printf("原始上下文地址: %p\n", c)
// 错误:在goroutine中直接使用原始上下文
go func() {
// 模拟异步处理(比如日志记录、数据分析等)
time.Sleep(200 * time.Millisecond)
// 问题1:上下文可能已经被回收
fmt.Printf("在goroutine中访问上下文地址: %p\n", c)
fmt.Printf("在goroutine中访问URL: %s\n", c.Request.URL.Path)
// 问题2:可能引发竞态条件
// 主goroutine可能已经完成了请求处理,释放了上下文
// 这里访问c的任何数据都可能导致panic或数据不一致
}()
c.Next()
fmt.Println("=== 错误示例结束 ===")
}
}
// CorrectGoroutineHandler 2. 正确的示例:使用上下文的只读副本
func CorrectGoroutineHandler() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("=== 正确示例开始 ===")
fmt.Printf("原始上下文地址: %p\n", c)
// 正确:创建上下文的只读副本
copyCtx := c.Copy()
fmt.Printf("副本上下文地址: %p\n", copyCtx)
go func(ctx *gin.Context) {
// 模拟异步处理
time.Sleep(200 * time.Millisecond)
// 安全:使用副本上下文
fmt.Printf("在goroutine中访问副本上下文地址: %p\n", ctx)
fmt.Printf("在goroutine中访问副本URL: %s\n", ctx.Request.URL.Path)
// 副本是只读的,不会影响原始请求
// 可以安全地读取请求信息,但不能修改响应
}(copyCtx)
c.Next()
fmt.Println("=== 正确示例结束 ===")
}
}
// RaceConditionDemoHandler 3. 演示竞态条件的示例
func RaceConditionDemoHandler() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("=== 竞态条件演示开始 ===")
// 模拟多个goroutine同时访问上下文
for i := 0; i < 3; i++ {
go func(index int) {
time.Sleep(time.Duration(index) * 100 * time.Millisecond)
// 这里可能发生竞态条件
// 不同的goroutine可能同时访问c的相同数据,导致竞态条件
fmt.Printf("Goroutine %d 访问URL: %s\n", index, c.Request.URL.Path)
}(i)
}
c.Next()
fmt.Println("=== 竞态条件演示结束 ===")
}
}
// SafeConcurrentHandler 4. 使用副本避免竞态条件的示例
func SafeConcurrentHandler() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("=== 安全并发示例开始 ===")
// 为每个goroutine创建独立的副本
for i := 0; i < 3; i++ {
copyCtx := c.Copy()
go func(index int, ctx *gin.Context) {
time.Sleep(time.Duration(index) * 100 * time.Millisecond)
// 每个goroutine使用自己的副本,避免竞态条件
fmt.Printf("Goroutine %d 访问副本URL: %s\n", index, ctx.Request.URL.Path)
}(i, copyCtx)
}
c.Next()
fmt.Println("=== 安全并发示例结束 ===")
}
}
// AsyncLoggingHandler 5. 实际应用场景:异步日志记录
func AsyncLoggingHandler() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
// 创建副本用于异步日志记录
logCtx := c.Copy()
// 异步记录请求日志(不影响主请求的响应时间)
go func(ctx *gin.Context, startTime time.Time) {
// 模拟日志写入(可能是文件、数据库等)
time.Sleep(50 * time.Millisecond)
duration := time.Since(startTime)
fmt.Printf("[ASYNC LOG] %s %s - %v\n",
ctx.Request.Method,
ctx.Request.URL.Path,
duration)
}(logCtx, start)
c.Next()
// 主请求继续处理,不等待日志记录完成
}
}
// AsyncDataProcessingHandler 6. 实际应用场景:异步数据处理
func AsyncDataProcessingHandler() gin.HandlerFunc {
return func(c *gin.Context) {
// 创建副本用于异步数据处理
dataCtx := c.Copy()
// 异步处理数据(比如发送到消息队列、更新统计信息等)
go func(ctx *gin.Context) {
// 模拟数据处理
time.Sleep(100 * time.Millisecond)
// 安全地读取请求数据
userAgent := ctx.Request.UserAgent()
ip := ctx.ClientIP()
fmt.Printf("[DATA PROCESSING] UserAgent: %s, IP: %s\n", userAgent, ip)
}(dataCtx)
c.Next()
}
}
func main() {
// 创建一个默认的路由引擎
r := gin.Default()
// 注册各种演示中间件
r.Use(AsyncLoggingHandler()) // 异步日志记录
r.Use(AsyncDataProcessingHandler()) // 异步数据处理
// 测试路由
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
"timestamp": time.Now().Unix(),
})
})
// 演示正确用法的路由
r.GET("/correct", CorrectGoroutineHandler(), func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "正确使用goroutine的示例",
})
})
// 演示安全并发的路由
r.GET("/safe", SafeConcurrentHandler(), func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "安全并发处理示例",
})
})
// 警告:演示错误用法的路由(实际生产环境应避免)
r.GET("/wrong", WrongGoroutineHandler(), func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "错误使用goroutine的示例(可能引发问题)",
})
})
// 警告:演示竞态条件的路由(实际生产环境应避免)
r.GET("/race", RaceConditionDemoHandler(), func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "竞态条件演示示例(可能引发问题)",
})
})
fmt.Println("服务器启动在 :8080")
fmt.Println("访问以下路由进行测试:")
fmt.Println(" GET /ping - 基础测试")
fmt.Println(" GET /correct - 正确用法演示")
fmt.Println(" GET /safe - 安全并发演示")
fmt.Println(" GET /wrong - 错误用法演示(谨慎使用)")
fmt.Println(" GET /race - 竞态条件演示(谨慎使用)")
r.Run(":8080")
}
Gin 处理 Cors 跨域
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Printf("请求路径:%s\n", c.Request.URL.Path)
fmt.Printf("请求方法:%s\n", c.Request.Method)
// 直接设置允许所有来源,移除域名白名单检查
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Headers",
"Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Accept, Origin, Cache-Control, X-Requested-With")
c.Header("Access-Control-Allow-Methods",
"POST, OPTIONS, GET, PUT, DELETE, PATCH")
c.Header("Access-Control-Max-Age", "86400") // 1天缓存
// 处理预检请求
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
func main() {
r := gin.Default()
// 给所有路由添加中间件
r.Use(Cors())
// 定义一个 GET 请求的路由,路径为 "/"
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "hello world",
})
})
// 启动 HTTP 服务器,默认监听在 0.0.0.0:8080
r.Run(":8080")
}
代码地址:
https://github.com/dengshuaix/go_gin_study
学习博客:
https://www.liwenzhou.com/posts/Go/gin/#c-9-3
