11 | 给 Gin 服务器添加中间件

提示:

在开发 Web 服务器时,经常会对所有的请求进行通用的处理,例如:打印请求的输入、输出,多所有请求进行认证鉴权等。这时候,不可能在每一个 API 接口中都实现相同的逻辑,不优雅,也很难维护。

这种情况下,业界通用的做法是,通过 Web 中间件来实现。本节课,就来看下如何基于 Gin 框架实现一个 Web 中间件。

Web 中间件介绍

中间件(Middleware)是位于应用程序请求-响应处理循环中的一个特殊函数。它可以在请求到达业务逻辑处理之前修改/处理请求,或是在响应返回给客户端之前修改/处理响应。中间件根据使用方又可分为客户端中间件和服务端中间件,两者在实现原理和使用方式上是一致的。

中间件的核心作用是对请求或响应进行预处理、后处理或监控。它允许在请求和响应被发送或接收之前或之后插入自定义逻辑,从而实现多种功能,例如认证、授权、日志记录、性能监控、错误处理、请求验证、跨域支持、限流等。以下是核心使用场景的详细说明:

  • 认证和授权: 使用中间件可以实现认证和授权逻辑。在中间件中,可以验证请求者的身份、权限等信息,并根据情况决定是否允许请求继续进行;
  • 日志记录: 中间件可以用于记录请求和响应的详细信息,从而实现日志记录和监控。可以记录请求的内容、调用的方法、响应的结果等,以便于调试和分析;
  • 错误处理: 在中间件中可以捕获和处理 gRPC 调用过程中可能发生的错误,以提供更友好的错误信息或进行恢复操作;
  • 性能监视: 使用中间件可以监视 Web 调用的性能指标,如调用时间、响应时间等,从而实现性能监控和优化。

Web 中间件工作原理如下图所示。

上图中,有两个中间件:中间件 A 和中间件 B。一个 Web 请求从开始到结束时的执行流程为:中间件 A->中间件 B->处理器函数->中间件 B->中间件 A,其执行顺序类似于栈结构。

Web 中间件的作用实际上是实现对请求的前置拦截和对响应的后置拦截功能:

  • 请求前置拦截: 在 Web 请求到达定义的处理器函数之前,对请求进行拦截并执行相应的处理;
  • 请求后置拦截: 在完成请求的处理并响应客户端后,拦截响应并进行相应的处理。

需要注意的是,中间件会附加到每个请求的链路上,因此如果中间件性能较差或不稳定,将会影响所有 API 接口。因此,在开发中间件时,应确保其稳定性和性能,同时建议仅添加必要的中间件。

Gin 中间件介绍

Gin 框架也支持 Web 中间件,在 Gin 框架中,Web 中间件就叫中间件。本节课就来详细介绍如何实现并添加 Gin 中间件。

Gin 支持三种中间件使用方式:

  • 全局中间件: 全局中间件会作用于所有的路由。它们通常用于处理通用功能,比如请求日志记录、跨域设置、错误恢复;
  • 路由组中间件: 路由组中间件仅对指定的路由组生效,适用于将某些逻辑限定在同一组相关的路由中。例如,所有/api 路径下的路由可能都需要一套特定的身份验证中间件;
  • 单个路由中间件: 单个路由中间件仅对一个路由起作用。有时某个路由需要执行独立的中间件逻辑,这种情况下,可以将中间件绑定到单个路由上。

不同路由组的中间件设置方式不同。下述代码展示了 Gin 中间件的开发和设置方法。

go 复制代码
package main

import (
    "log"
    "net/http"

    "github.com/gin-gonic/gin"
)

// 定义一个通用中间件:打印请求路径
func LogMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        log.Printf("Request path: %s\n", c.Request.URL.Path)
        // 继续处理后续的中间件或路由
        c.Next()
    }
}

func main() {
    r := gin.Default()

    // 使用全局中间件:所有路由都会经过该中间件
    // r.Use(gin.Logger(), gin.Recovery()) 同时设置多个 Gin 中间件
    r.Use(LogMiddleware())

    // 定义普通路由
    r.GET("/", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"message": "Home"})
    })

    // 定义一个路由组,并为组添加中间件
    apiGroup := r.Group("/api", LogMiddleware())
    {
        apiGroup.GET("/hello", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"message": "Hello, API"})
        })
        apiGroup.GET("/world", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"message": "World, API"})
        })
    }

    // 为单个路由添加中间件
    r.GET("/secure", LogMiddleware(), func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"message": "This is a secure route"})
    })

    // 启动HTTP服务
    r.Run(":8080") // 监听在8080端口
}

上述代码中,通过 r.Use()r.Group()r.Get() 方法分别设置了多个 Gin 中间件。在设置 Gin 中间件时,可以根据需要同时设置一个或者多个,例如 r.Use(gin.Logger(), gin.Recovery()),同时设置了两个 Gin 中间件。

LogMiddleware 中间件中,c.Next() 方法之前的代码将在请求到达处理器函数之前执行,而 c.Next() 方法之后的代码将在请求经过处理器函数处理之后执行。另外,在开发 Gin 中间件时,c.Abort() 方法也经常被开发者使用,该方法会直接终止请求的执行。

fastgo 添加中间件

给 fastgo 添加 Gin 中间件包括以下 2 步:

  1. 开发 Gin 中间件;
  2. 加载 Gin 中间件。

开发 Gin 中间件

这里,我们先开发 3 个常用的 Gin 中间件:

  • NoCache: 通过设置一些 Header,禁止客户端缓存 HTTP 请求的返回结果;
  • Cors: 用来设置 options 请求的返回头,然后退出中间件链,并结束请求(浏览器跨域设置);
  • RequestID: 用来在每一个 HTTP 请求的 context, response 中注入 x-request-id 键值对。

Gin 中间件其实就是一个 func(c *gin.Context) 类型的函数。在函数中可以从 c中解析请求,执行通用的处理逻辑、设置返回参数等。

NoCache 及 Cors 中间件代码如下(位于 internal/pkg/middleware/header.go 文件中):

go 复制代码
// NoCache 是一个 Gin 中间件,用来禁止客户端缓存 HTTP 请求的返回结果.
func NoCache(c *gin.Context) {
    c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
    c.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
    c.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
    c.Next()
}

// Cors 是一个 Gin 中间件,用来设置 options 请求的返回头,然后退出中间件链,并结束请求(浏览器跨域设置).
func Cors(c *gin.Context) {
    if c.Request.Method != "OPTIONS" {
        c.Next()
    } else {
        c.Header("Access-Control-Allow-Origin", "*")
        c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
        c.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept")
        c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
        c.Header("Content-Type", "application/json")
        c.AbortWithStatus(200)
    }
}

上述代码,通过设置返回头来实现相关的中间件功能。

RequestID 中间件位于 internal/pkg/middleware/requestid.go 文件中,代码如下:

go 复制代码
// RequestID 是一个 Gin 中间件,用来在每一个 HTTP 请求的 context, response 中注入 `x-request-id` 键值对.
func RequestID() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 从请求头中获取 `x-request-id`,如果不存在则生成新的 UUID
        requestID := c.Request.Header.Get(known.XRequestID)

        if requestID == "" {
            requestID = uuid.New().String()
        }

        // 将 RequestID 保存到 context.Context 中,以便后续程序使用
        ctx := contextx.WithRequestID(c.Request.Context(), requestID)
        c.Request = c.Request.WithContext(ctx)

        // 将 RequestID 保存到 HTTP 返回头中,Header 的键为 `x-request-id`
        c.Writer.Header().Set(known.XRequestID, requestID)

        // 继续处理请求
        c.Next()
    }
}

RequestID 中间件尝试从 c中获取 x-request-id请求头,如果获取不到则生成一个新的请求 ID 并设置为请求头 x-request-id的值。为了能够在代码中方便的获取请求 ID,例如:可以打印到日志中,方便串联整个请求日志。还通过 contextx.WithRequestID 调用,将请求 ID 保存在了自定义上下文 contextx 中。

中间件最后通过 c.Writer.Header().Set(known.XRequestID, requestID) 调用,将请求 ID 设置到了返回头中,这样可以将请求 ID 也头传给客户端。方便出问题时提供请求 ID 进行排查。

因为 x-request-id会在多个地方被访问,为了更好的管理这种通用的常量,将 x-request-id以常量的形式保存在 known 包中(位于 internal/pkg/known/known.go 文件中),包内容如下:

go 复制代码
package known

const (
    // XRequestID 用来定义上下文中的键,代表请求 ID.
    XRequestID = "x-request-id"
)

为了能够很方便的从 context.Context 中获取请求 ID,fastgo 项目引入了自定义上下文包 contextx。contextx 提供了便捷的函数,给 context.Context 添加键值对,并从 context.Conext 中根据键获取对应的值。contextx 包代码位于 internal/pkg/contextx/contextx.go 文件中,内容如下:

go 复制代码
package contextx

import (
    "context"
)

// 定义用于上下文的键.
type (
    // requestIDKey 定义请求 ID 的上下文键.
    requestIDKey struct{}
)

// WithRequestID 将请求 ID 存放到上下文中.
func WithRequestID(ctx context.Context, requestID string) context.Context {
    return context.WithValue(ctx, requestIDKey{}, requestID)
}

// RequestID 从上下文中提取请求 ID.
func RequestID(ctx context.Context) string {
    requestID, _ := ctx.Value(requestIDKey{}).(string)
    return requestID
}

这里要注意,他通过定义一个新的类型 requestIDKey struct{},可以避免键名冲突。因为类型是唯一的。

加载 Gin 中间件

修改 internal/apiserver/server.go 文件,在文件中添加以下代码行,来加载 Gin 路由:

go 复制代码
package apiserver

import (
    ...
    mw "github.com/onexstack/fastgo/internal/pkg/middleware"
    ...
)
...
// NewServer 根据配置创建服务器.
func (cfg *Config) NewServer() (*Server, error) {
    ...
    // gin.Recovery() 中间件,用来捕获任何 panic,并恢复
    mws := []gin.HandlerFunc{gin.Recovery(), mw.NoCache, mw.Cors, mw.RequestID()}
    engine.Use(mws...)
    ...
}

编译并运行

执行以下命令编译并运行 fg-apiserver:

bash 复制代码
$ ./build.sh
$ _output/fg-apiserver -c configs/fg-apiserver.yaml

打来一个新的 Linux 终端,并执行以下命令:

bash 复制代码
$ curl http://127.0.0.1:6666/healthz -v
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 6666 (#0)
> GET /healthz HTTP/1.1
> Host: 127.0.0.1:6666
> User-Agent: curl/7.64.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate, value
< Content-Type: application/json; charset=utf-8
< Expires: Thu, 01 Jan 1970 00:00:00 GMT
< Last-Modified: Sat, 08 Mar 2025 01:57:11 GMT
< X-Request-Id: 4e27e3ec-85c9-4ffc-8f0d-fa84c8053d19
< Date: Sat, 08 Mar 2025 01:57:11 GMT
< Content-Length: 15
< 
* Connection #0 to host 127.0.0.1 left intact
{"status":"ok"}

可以看到,请求返回投中,成功返回了请求 ID:X-Request-Id,及 NoCache 中间件设置的返回头。

相关推荐
移远通信几秒前
移远通信联合德壹发布全球首款搭载端侧大模型的AI具身理疗机器人
人工智能
Z_W_H_2 分钟前
【AI】Stable Diffusion安装
人工智能·stable diffusion
Earth explosion18 分钟前
如何优化AI模型的Prompt:深度指南
人工智能·prompt
淞宇智能科技33 分钟前
欧姆龙PLC学习的基本步骤
人工智能·自动化·电气设计·技术资料
皮皮虾123441 分钟前
有哪些好用的AI视频加工创作网站
人工智能
邪恶的贝利亚42 分钟前
多种注意力机制(文本->残差->视频)
人工智能·深度学习
q567315231 小时前
用PHP的Guzzle库编写的图片爬虫程序
android·开发语言·爬虫·http·golang·php
weixi_kelaile5201 小时前
ai智能语音机器人对我们生活有什么影响
java·linux·服务器·人工智能·机器人·生活
果冻人工智能1 小时前
古生物学家与人工智能的较量
人工智能
梓羽玩Python1 小时前
AI浏览器操控革命!MCP-Playwright:AI自动化神器,可执行JS代码进行复杂交互任务!
人工智能·github