使用 Gin 中间件处理超时时,遇到 “Headers were already written. Wanted to override status code 504 with 500” 的警告。这是因为超时处理逻辑试图修改已经发送给客户端的响应头信息,导致冲突。
我们可以使用 Go 语言中的通道(channel)来传递数据,并根据超时状态进行不同的响应处理。
示例代码
package main
import (
"context"
"errors"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// 定义一个结构体用于封装响应数据
type ResponseData struct {
StatusCode int `json:"status_code"` // HTTP 状态码
Message string `json:"message,omitempty"` // 响应消息
Data interface{} `json:"data,omitempty"` // 响应数据
Error error `json:"error,omitempty"` // 错误信息
}
// 定义一个超时中间件
func Timeout(timeout time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
// 创建一个带超时时间的上下文
ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
defer cancel()
// 将新的 Context 对象设置到 Gin 上下文中
c.Request = c.Request.WithContext(ctx)
// 创建一个用于传递响应数据的通道
resChan := make(chan ResponseData)
// 异步执行请求处理逻辑
go func() {
// 将通道存储在 Gin 上下文中,以便后续处理函数可以访问
c.Set("ResChan", resChan)
// 执行后续的处理逻辑,例如路由处理函数
c.Next()
// 关闭通道,通知其他 Goroutine 数据已发送完毕
close(resChan)
}()
// 等待通道响应或超时
select {
case res := <-resChan:
// 处理响应数据
if res.Error != nil {
c.JSON(res.StatusCode, gin.H{"error": res.Error.Error()})
return
}
c.JSON(res.StatusCode, gin.H{
"message": res.Message,
"data": res.Data,
})
case <-ctx.Done():
// 处理超时
if ctx.Err() == context.DeadlineExceeded {
c.JSON(http.StatusGatewayTimeout, gin.H{"error": "Service is unavailable or timed out"})
c.Abort()
return
}
}
}
}
// 定义一个处理注册请求的结构体
type AuthHandler struct {
authService *AuthService // 依赖注入
}
// 处理注册请求的函数
func (h *AuthHandler) Register(c *gin.Context) {
// 从 Gin 上下文中获取通道
resChan := c.MustGet("ResChan").(chan ResponseData)
// 处理请求数据
// 示例:假设我们需要解析请求的 JSON 数据
var requestData map[string]interface{}
if err := c.ShouldBindJSON(&requestData); err != nil {
// 如果解析请求数据时发生错误,发送错误信息到通道
resChan <- ResponseData{
StatusCode: http.StatusBadRequest,
Error: errors.New("invalid request data"),
}
return
}
// 处理业务逻辑
// 示例:假设我们调用某个服务进行注册处理
result, err := h.authService.Register(requestData)
if err != nil {
// 如果业务逻辑处理时发生错误,发送错误信息到通道
resChan <- ResponseData{
StatusCode: http.StatusInternalServerError,
Error: err,
}
} else {
// 否则,发送成功信息和数据到通道
resChan <- ResponseData{
StatusCode: http.StatusCreated,
Message: "Registration completed successfully.",
Data: result,
}
}
}
func main() {
// 初始化 Gin 框架
r := gin.Default()
// 创建一个 AuthHandler 实例
authHandler := &AuthHandler{
authService: &AuthService{}, // 假设我们有一个 AuthService 类型的实例
}
// 使用超时中间件,设置超时时间为 5 秒
r.Use(Timeout(5 * time.Second))
// 注册路由
r.POST("/register", authHandler.Register)
// 启动服务
r.Run(":8080")
}
// 假设有一个 AuthService 类型,用于处理注册逻辑
type AuthService struct{}
func (s *AuthService) Register(data map[string]interface{}) (interface{}, error) {
// 处理注册逻辑
// 示例:假设总是成功
return map[string]string{"user_id": "12345"}, nil
}
代码解释
- 超时上下文: 使用
context.WithTimeout()
创建一个带有超时时间的上下文,并在超时后自动取消,防止请求长时间阻塞。 - 通道传递数据: 创建一个通道用于在超时中间件和请求处理函数之间传递数据,避免直接修改响应头信息。
- 异步处理请求: 使用 Goroutine 异步执行请求处理逻辑,防止阻塞主线程,提高服务器并发处理能力。
- select 监听通道: 使用
select
语句同时监听超时通道和数据通道,根据不同情况进行处理。 - 中断后续处理: 在超时情况下,使用
c.Abort()
函数中断后续的中间件和路由处理函数,避免重复响应。