8-Gin 中间件 --[Gin 框架入门精讲与实战案例] 【文末有测试代码】
路由中间件
Gin 是一个用 Go (Golang) 编写的 HTTP web 框架。它以性能好、中间件支持灵活著称,非常适合用来构建微服务或 RESTful API 服务。下面我将提供三个使用 Gin 的路由中间件的完整示例。
示例 1: 简单的日志记录中间件
这个中间件会在每个请求处理前后打印日志信息,包括请求的方法、路径和响应状态码。
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// Logger 返回一个日志中间件,用于记录HTTP请求的详细信息。
// 该中间件主要记录了请求的路径、查询字符串、响应状态码、客户端IP和请求处理时间。
// 它通过gin.HandlerFunc类型返回一个闭包函数,以便在Gin路由中使用。
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
// 记录请求开始时间
start := time.Now()
// 获取请求路径
path := c.Request.URL.Path
// 获取查询字符串
raw := c.Request.URL.RawQuery
// 处理请求前
c.Next() // 处理请求
// 处理请求后
// 记录请求结束时间
end := time.Now()
// 计算请求处理耗时
latency := end.Sub(start)
// 获取HTTP响应状态码
statusCode := c.Writer.Status()
// 获取客户端IP
clientIP := c.ClientIP()
// 打印日志信息
log.Printf("| %3d | %13v | %15s | %40s |",
statusCode, latency, clientIP, path+"?"+raw)
}
}
func main() {
// 初始化Gin默认引擎
r := gin.Default()
// 使用Logger中间件
r.Use(Logger())
// 定义一个模拟耗时操作的测试路由
r.GET("/test", func(c *gin.Context) {
// 模拟耗时操作
time.Sleep(5 * time.Second)
// 返回JSON响应
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
// 启动HTTP服务器,监听端口8080
r.Run(":8080")
}
示例 2: 认证中间件
这个中间件会检查每个请求中是否包含特定的 Authorization
header,并验证其值是否为预期的 token。
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
// AuthRequired 是一个中间件函数,用于验证请求是否具有有效的授权令牌。
// 它通过检查HTTP请求的头部中的 "Authorization" 字段是否匹配预设的令牌来实现。
// 如果令牌无效,它会中止请求并返回 "Unauthorized" 错误。
// 如果令牌有效,它会允许请求继续进行到下一个处理函数。
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
c.Request.Header.Set("Authorization", "my-secret-token")
token := c.GetHeader("Authorization")
fmt.Println("Token:", token)
if token != "my-secret-token" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
c.Next()
}
}
func main() {
r := gin.Default()
// 应用认证中间件到所有 /admin 路由
// 这里通过将 AuthRequired 作为参数传递给 r.Group 来实现,
// 确保所有以 "/admin" 开头的路由都受到保护,防止未经授权的访问。
admin := r.Group("/admin", AuthRequired())
{
// 定义 /admin/dashboard 路由的处理函数。
// 该函数仅在通过 AuthRequired 中间件的验证后执行,
// 确保只有拥有有效令牌的请求才能访问dashboard。
admin.GET("/dashboard", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Welcome to the dashboard"})
})
}
// 启动HTTP服务器,监听端口8080。
r.Run(":8080")
}
示例 3: 自定义恢复中间件
当应用程序遇到未捕获的异常(panic)时,这个中间件会捕获并恢复程序,返回一个标准的错误消息给客户端。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// CustomRecovery 自定义恢复中间件,用于捕获并处理panic
// 这个中间件的目的是当发生panic时,能够优雅地恢复并返回500错误响应
func CustomRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// 如果有 panic 发生,设置状态码为 500 并返回错误信息
c.Error(err.(error)).SetMeta("recovered")
c.AbortWithStatusJSON(http.StatusInternalServerError,
gin.H{"error": "Internal Server Error"})
}
}()
c.Next()
}
}
func main() {
r := gin.Default()
// 使用自定义的恢复中间件
r.Use(CustomRecovery())
// 设置一个路由,访问该路由会触发panic
r.GET("/panic", func(c *gin.Context) {
panic("an error occurred!")
// fmt.Print("This line will not be executed.")
})
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "Hello, World!"})
})
// 启动服务器,监听8080端口
r.Run(":8080")
}
在上述示例中,我们创建了三种不同类型的中间件:用于日志记录、用户认证和异常恢复。你可以根据需要组合这些中间件,或者创建更多自定义中间件来满足你的应用需求。
全局中间件
在 Gin 框架中,全局中间件是应用于所有路由请求的中间件。你可以通过 gin.Engine.Use()
方法来注册这些中间件。下面我将给出三个使用全局中间件的完整示例,涵盖日志记录、异常恢复和性能监控。
示例 1: 全局日志记录中间件
这个中间件会在每个请求处理前后打印日志信息,包括请求的方法、路径、客户端 IP 和响应状态码。
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// GlobalLogger 返回一个gin.HandlerFunc类型的中间件,用于全局日志记录
// 它记录了请求的来源IP、路径、查询参数以及请求处理时间
func GlobalLogger() gin.HandlerFunc {
return func(c *gin.Context) {
// 记录请求开始时间
start := time.Now()
// 获取请求路径
path := c.Request.URL.Path
// 获取查询参数
raw := c.Request.URL.RawQuery
// 获取客户端IP
clientIP := c.ClientIP()
// 日志记录:开始处理请求
log.Printf("Start handling request from %s: %s?%s", clientIP, path, raw)
// 继续处理请求链中的其他处理器和中间件
c.Next()
// 记录请求结束时间
end := time.Now()
// 计算请求处理耗时
latency := end.Sub(start)
// 获取HTTP响应状态码
statusCode := c.Writer.Status()
// 日志记录:请求处理结果,包括状态码、耗时、客户端IP和请求路径
log.Printf("| %3d | %13v | %15s | %40s |\n",
statusCode, latency, clientIP, path+"?"+raw)
}
}
func main() {
// 创建默认的gin路由器
r := gin.Default()
// 添加全局中间件:使用GlobalLogger记录所有请求的信息
r.Use(GlobalLogger())
// 定义路由:GET请求到/ping路径
r.GET("/ping", func(c *gin.Context) {
// 模拟耗时操作:休眠2秒
time.Sleep(2 * time.Second)
// 响应JSON数据:{"message": "pong"}
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
// 启动服务器,监听8080端口
r.Run(":8080")
}
示例 2: 全局异常恢复中间件
当应用程序遇到未捕获的异常(panic)时,这个中间件会捕获并恢复程序,返回一个标准的错误消息给客户端。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// CustomRecovery 自定义异常恢复中间件
// 该中间件用于捕获并处理控制器中的 panic
// 返回: gin.HandlerFunc 类型的中间件函数
func CustomRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// 如果有 panic 发生,设置状态码为 500 并返回错误信息
c.Error(err.(error)).SetMeta("recovered")
c.AbortWithStatusJSON(http.StatusInternalServerError,
gin.H{"error": "Internal Server Error"})
}
}()
c.Next()
}
}
func main() {
r := gin.Default()
// 添加全局异常恢复中间件
r.Use(CustomRecovery())
// 定义可能引发 panic 的路由
r.GET("/panic", func(c *gin.Context) {
panic("an error occurred!")
})
r.Run(":8080")
}
示例 3: 全局性能监控中间件
这个中间件用于记录每个请求的处理时间,并且可以根据需要收集更多的性能指标。
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// PerformanceMonitor 是一个中间件函数,用于监控请求的性能。
// 它会记录每个请求的处理时间。
func PerformanceMonitor() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
// 继续处理请求链中的其他处理器和中间件
c.Next()
// 请求处理后的操作
end := time.Now()
latency := end.Sub(start)
log.Printf("Request completed in %v\n", latency)
}
}
func main() {
r := gin.Default()
// 添加全局性能监控中间件
r.Use(PerformanceMonitor())
// 定义一些路由
r.GET("/slow", func(c *gin.Context) {
time.Sleep(5 * time.Second) // 模拟耗时操作
c.JSON(http.StatusOK, gin.H{
"message": "This is a slow endpoint",
})
})
r.Run(":8080")
}
在这三个示例中,我们展示了如何创建和应用全局中间件来增强 Gin 应用的功能。GlobalLogger
中间件记录了每个请求的信息,CustomRecovery
中间件提供了异常恢复机制,而 PerformanceMonitor
中间件则可以用来监控应用程序的性能。你可以根据自己的需求调整这些中间件或添加新的全局中间件。
在路由分组中配置中间件
在 Gin 框架中,路由分组(Group)允许你将一组相关的路由归类在一起,并且可以为这个组配置特定的中间件。这有助于保持代码的整洁和模块化,同时提供更细粒度的控制。下面是三个使用路由分组并配置中间件的完整示例。
示例 1: API 版本控制
这个例子展示了如何创建不同版本的 API 分组,并为每个版本添加不同的中间件。
package main
import (
"github.com/gin-gonic/gin"
)
// VersionMiddleware 返回一个中间件处理器,用于在上下文中设置API版本信息。
// 参数 version 指定要设置的API版本。
// 该中间件使得后续的处理器可以访问通过c.Get("api-version")获取版本信息。
func VersionMiddleware(version string) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("api-version", version)
c.Next()
}
}
func main() {
r := gin.Default()
// 创建并配置/v1版本的API路由组。
// 使用VersionMiddleware设置API版本为"1.0"。
v1 := r.Group("/v1", VersionMiddleware("1.0"))
{
// 处理/v1版本的GET请求,返回用户列表信息和API版本。
v1.GET("/users", func(c *gin.Context) {
version := c.MustGet("api-version").(string)
c.JSON(200, gin.H{"version": version, "message": "Users list"})
})
}
// 创建并配置/v2版本的API路由组。
// 使用VersionMiddleware设置API版本为"2.0"。
v2 := r.Group("/v2", VersionMiddleware("2.0"))
{
// 处理/v2版本的GET请求,返回用户列表信息和API版本,可能包含新特性。
v2.GET("/users", func(c *gin.Context) {
version := c.MustGet("api-version").(string)
c.JSON(200, gin.H{"version": version, "message": "Users list with new features"})
})
}
// 启动HTTP服务器,监听端口8080。
r.Run(":8080")
}
示例 2: 用户认证中间件应用于部分路由
这个例子展示了如何为需要用户认证的路由分组添加认证中间件。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// AuthMiddleware 返回一个中间件函数,用于验证请求的授权令牌
// 如果令牌有效,请求将继续处理;如果令牌无效,将返回401 Unauthorized错误
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token != "my-secret-token" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
c.Next()
}
}
func main() {
r := gin.Default()
// 不需要认证的公开路由
public := r.Group("/public")
{
public.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong"})
})
}
// 需要认证的受保护路由
// 使用AuthMiddleware中间件来验证请求的授权令牌
protected := r.Group("/protected", AuthMiddleware())
{
protected.GET("/dashboard", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Welcome to the dashboard"})
})
}
r.Run(":8080")
}
示例 3: 结合全局中间件与分组中间件
此示例展示了如何结合全局中间件和分组中间件,以确保所有请求都经过日志记录中间件,而只有特定的分组才需要通过额外的中间件。
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
// 请求处理前的操作
log.Printf("Start handling request: %s?%s", path, raw)
// 继续处理请求链中的其他处理器和中间件
c.Next()
// 请求处理后的操作
end := time.Now()
latency := end.Sub(start)
statusCode := c.Writer.Status()
clientIP := c.ClientIP()
log.Printf("| %3d | %13v | %15s | %40s |\n",
statusCode, latency, clientIP, path+"?"+raw)
}
}
func RateLimitMiddleware(limit int) gin.HandlerFunc {
return func(c *gin.Context) {
// 这里只是一个简单的示例,实际应用中应该实现更复杂的限流逻辑
time.Sleep(time.Duration(limit) * time.Millisecond)
c.Next()
}
}
func main() {
r := gin.Default()
// 添加全局中间件
r.Use(LoggerMiddleware())
// 创建一个没有额外中间件的公共路由分组
public := r.Group("/public")
{
public.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong"})
})
}
// 创建一个有速率限制的分组
limit := r.Group("/limited", RateLimitMiddleware(100))
{
limit.GET("/slow", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "This is a rate-limited endpoint"})
})
}
r.Run(":8080")
}
在这三个示例中,我们展示了如何使用中间件来增强 Gin 路由的功能。你可以根据自己的需求组合全局中间件和分组中间件,以实现更复杂的应用逻辑。
中间件和对应控制器之间共享数据
在 Gin 框架中,中间件和控制器之间共享数据的一个常用方法是使用 gin.Context
的 Set()
和 Get()
方法。gin.Context
是一个上下文对象,它贯穿整个请求的生命周期,因此可以在中间件中设置一些值,并在之后的控制器处理函数中获取这些值。
下面是三个展示如何在中间件和控制器之间共享数据的完整示例:
示例 1: 用户认证信息共享
在这个例子中,我们将在认证中间件中解析用户的认证信息,并将其存储到 gin.Context
中,以便后续的控制器可以访问这些信息。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// AuthMiddleware 是一个中间件,用于验证请求的授权信息。
// 它通过检查请求头中的 Authorization 字段来确定请求是否经过授权。
// 如果验证通过,它会设置用户信息并允许请求继续进行;
// 如果验证失败,它会返回 401 状态码和错误信息。
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "my-secret-token" {
c.Set("user", "authenticated-user") // 设置用户信息
c.Next()
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
}
}
}
func main() {
r := gin.Default()
// 创建一个受保护的路由组,所有该组内的请求都需要经过授权验证。
protected := r.Group("/protected", AuthMiddleware())
{
// 处理 /protected/profile 路径的 GET 请求。
// 如果请求已授权,返回欢迎信息;否则,返回错误信息。
protected.GET("/profile", func(c *gin.Context) {
if user, exists := c.Get("user"); exists {
c.JSON(http.StatusOK, gin.H{"message": "Welcome " + user.(string)})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "User info not found"})
}
})
}
// 启动 HTTP 服务器,监听 8080 端口。
r.Run(":8080")
}
示例 2: 请求计数器
这个例子展示了如何在中间件中维护一个简单的请求计数器,并在控制器中访问这个计数器。
package main
import (
"net/http"
"sync"
"github.com/gin-gonic/gin"
)
// requestCount 记录HTTP请求的次数
var (
requestCount int
mu sync.Mutex
)
// RequestCounter 是一个中间件函数,用于在每次HTTP请求时增加请求计数
// 该函数首先锁定互斥锁以确保并发安全,然后增加请求计数,并将计数存储在gin.Context中
// RequestCounter 返回一个中间件函数,用于统计HTTP请求的数量。
// 该中间件使用了互斥锁确保并发安全,特别是在处理高并发请求时。
// 它在每个请求的上下文中设置了当前的请求计数,供后续处理使用。
func RequestCounter() gin.HandlerFunc {
return func(c *gin.Context) {
// 上锁以确保并发安全
mu.Lock()
// 增加请求计数
requestCount++
// 在上下文中设置当前的请求计数
c.Set("requestCount", requestCount)
// 解锁以释放资源
mu.Unlock()
// 调用链中的下一个处理函数
c.Next()
}
}
func main() {
r := gin.Default()
// 使用RequestCounter中间件来跟踪和增加请求计数
r.Use(RequestCounter())
// 处理/count路由的GET请求,返回当前请求计数
r.GET("/count", func(c *gin.Context) {
if count, exists := c.Get("requestCount"); exists {
c.JSON(http.StatusOK, gin.H{"count": count})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Request count not found"})
}
})
// 启动HTTP服务器,监听8080端口
r.Run(":8080")
}
示例 3: 查询参数预处理
此示例展示了如何在中间件中解析并预处理查询参数,然后将处理后的结果传递给控制器。
package main
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// QueryParamProcessor 是一个中间件函数,用于处理查询参数。
// 它从请求中获取 'search' 查询参数,
// 对其进行一些处理(例如去除首尾空白字符),
// 并将处理后的结果存储在上下文中,键为 'processedQuery'。
// 这样后续的处理器可以访问到处理后的查询参数。
func QueryParamProcessor() gin.HandlerFunc {
return func(c *gin.Context) {
query := c.Query("search")
processedQuery := strings.TrimSpace(query) // 假设这里有一些复杂的处理逻辑
fmt.Println("Processed query:", processedQuery)
c.Set("processedQuery", processedQuery)
c.Next()
}
}
func main() {
r := gin.Default()
// 使用全局中间件 QueryParamProcessor。
// 该中间件会预处理所有传入请求的查询参数,
// 使任何后续路由处理器都可以访问处理后的查询参数。
r.Use(QueryParamProcessor())
// 定义 '/search' 的 GET 路由。
// 该路由检查上下文中是否存在 'processedQuery'。
// 如果存在,则返回包含处理后查询参数的搜索结果;
// 否则,返回一条消息表示未提供查询参数。
r.GET("/search", func(c *gin.Context) {
if query, exists := c.Get("processedQuery"); exists {
c.JSON(http.StatusOK, gin.H{"search_results": "Results for: " + query.(string)})
} else {
c.JSON(http.StatusOK, gin.H{"search_results": "No query provided"})
}
})
// 启动 Gin 服务器,并监听 8080 端口。
r.Run(":8080")
}
在这三个示例中,我们展示了不同的场景下如何利用 gin.Context
的 Set()
和 Get()
方法来在中间件和控制器之间共享数据。这种方式不仅可以简化代码逻辑,还能确保数据在整个请求处理过程中的一致性和可访问性。请注意,在并发环境中操作共享变量时(如示例2中的 requestCount
),应采取适当的同步措施以保证线程安全。
中间件注意事项
在使用 Gin 框架的中间件时,有一些重要的注意事项可以帮助你更安全、有效地编写和使用中间件。以下是一些关键点:
1. 中间件链的顺序
- 中间件的执行顺序是按照它们被添加到路由或路由组中的顺序来决定的。因此,先添加的中间件会最先处理请求,并且最后处理响应。
- 如果有多个中间件,确保它们的顺序符合你的逻辑需求。例如,日志记录中间件通常应该放在其他中间件之前,以便它可以记录整个请求过程。
2. c.Next()
的调用
- 在中间件中一定要调用
c.Next()
方法,它表示继续处理请求链中的下一个处理器或中间件。如果不调用c.Next()
,那么后续的处理器将不会被执行。 - 如果某个中间件需要阻止请求继续前进(如认证失败),可以调用
c.Abort()
或c.AbortWithStatus()
来停止链的执行。
3. 线程安全
- 如果你在中间件中共享了某些变量或资源,确保这些资源是线程安全的。Gin 是并发处理请求的,所以多个 goroutine 可能同时访问这些资源。
- 使用互斥锁(
sync.Mutex
)或者其他同步机制来保护共享状态。
4. 避免不必要的延迟
- 中间件不应该引入不必要的延迟。如果一个中间件需要执行耗时操作,考虑将其移到后台 goroutine 中,或者异步处理。
- 对于性能敏感的应用程序,尽量减少中间件的数量和复杂度。
5. 错误处理
- 中间件中应包含适当的错误处理逻辑。比如,在遇到未捕获的 panic 时,应该使用恢复中间件(recovery middleware)来捕获异常并返回合适的 HTTP 状态码。
- 当发生错误时,不要忘记调用
c.Error()
方法记录错误信息,这有助于后续的日志分析和调试。
6. 数据共享
- 使用
gin.Context.Set()
和gin.Context.Get()
方法在中间件与控制器之间传递数据。但是要注意,不要过度依赖这种方式,因为过多的状态管理会使代码难以维护。 - 如果你需要在整个应用中频繁访问某些数据,考虑使用配置文件或者依赖注入的方式来提供这些数据。
7. 清理工作
- 如果中间件分配了资源(如打开文件、网络连接等),确保在请求完成后正确释放这些资源。你可以使用
defer
关键字来确保即使发生错误也能执行清理代码。
8. 测试
- 编写单元测试来验证中间件的行为非常重要。Gin 提供了良好的测试支持,可以模拟 HTTP 请求来测试中间件和控制器的行为。
9. 文档和注释
- 给中间件添加清晰的注释和文档,解释它的功能和预期行为,这对于团队协作和后期维护都非常有用。
遵循上述注意事项,可以帮助你构建更加稳健和高效的 Gin 应用程序。记住,中间件是强大的工具,但也要谨慎使用,以避免引入不必要的复杂性。
下载示例代码
Gin 框架入门精讲与实战案例代码