Go Gin 快速学习 【Gin 中间件】
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
学习博客:

浙公网安备 33010602011771号