Context 复制

Context 不是线程安全的,不能在 goroutine 中直接使用。但有时候我们需要在后台异步处理一些任务,这时就需要复制 Context。

为什么需要复制

// 错误示例:直接在 goroutine 中使用 Context
func handler(c *gin.Context) {
    go func() {
        // 危险!Context 不是线程安全的
        userID := c.GetInt("userID")
        c.JSON(200, gin.H{"user_id": userID})
    }()
}

上面的代码可能导致数据竞争和不可预期的行为。

Copy 方法

Gin 提供了 Copy 方法来创建 Context 的只读副本:

func handler(c *gin.Context) {
    // 复制 Context
    cCp := c.Copy()
    
    go func() {
        // 安全使用副本
        userID := cCp.GetInt("userID")
        // 处理异步任务...
        log.Printf("Async task for user %d", userID)
    }()
    
    c.JSON(200, gin.H{"message": "request accepted"})
}

Copy 的工作原理

Copy 方法会创建一个新的 Context,包含以下内容的副本:

  • 请求信息(Request)
  • 响应写入器(Writer)
  • 存储的值(Keys)
  • 其他上下文数据

但注意:复制后的 Context 是只读的,不能用于写入响应。

实际应用示例

异步日志记录

func auditMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()
        
        // 异步记录审计日志
        cCp := c.Copy()
        go func() {
            logEntry := AuditLog{
                UserID:    cCp.GetInt("userID"),
                Method:    cCp.Request.Method,
                Path:      cCp.Request.URL.Path,
                Status:    cCp.Writer.Status(),
                Duration:  time.Since(start),
                IP:        cCp.ClientIP(),
                Timestamp: time.Now(),
            }
            saveAuditLog(logEntry)
        }()
    }
}

异步发送通知

func orderHandler(c *gin.Context) {
    var order Order
    if err := c.ShouldBindJSON(&order); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    // 保存订单
    err := saveOrder(&order)
    if err != nil {
        c.JSON(500, gin.H{"error": "failed to save order"})
        return
    }
    
    // 异步发送通知
    cCp := c.Copy()
    go func() {
        userID := cCp.GetInt("userID")
        sendOrderNotification(userID, order.ID)
    }()
    
    c.JSON(200, gin.H{"order_id": order.ID})
}

后台数据处理

func uploadHandler(c *gin.Context) {
    file, err := c.FormFile("file")
    if err != nil {
        c.JSON(400, gin.H{"error": "no file uploaded"})
        return
    }
    
    // 保存文件
    filename := saveUploadedFile(file)
    
    // 异步处理文件(如生成缩略图、提取文本等)
    cCp := c.Copy()
    go func() {
        userID := cCp.GetInt("userID")
        processFile(filename, userID)
    }()
    
    c.JSON(200, gin.H{"filename": filename})
}

注意事项

1. 不能写入响应

func handler(c *gin.Context) {
    cCp := c.Copy()
    
    go func() {
        // 这不会生效!副本不能写入响应
        cCp.JSON(200, gin.H{"message": "async"})
    }()
    
    c.JSON(200, gin.H{"message": "sync"})
}

2. Request Body 只能读取一次

func handler(c *gin.Context) {
    // 读取 body
    body, _ := c.GetRawData()
    
    cCp := c.Copy()
    go func() {
        // 副本中的 body 已经被读取过了
        // 需要在复制前保存需要的数据
        processBody(body)
    }()
}

3. 复制时机

在启动 goroutine 之前复制:

func handler(c *gin.Context) {
    // 先复制
    cCp := c.Copy()
    
    // 再启动 goroutine
    go func() {
        doSomething(cCp)
    }()
    
    c.Next()
}

完整示例

package main

import (
    "log"
    "net/http"
    "time"
    
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.New()
    r.Use(gin.Recovery())
    
    // 模拟认证中间件
    r.Use(func(c *gin.Context) {
        c.Set("userID", 123)
        c.Set("role", "admin")
        c.Next()
    })
    
    r.POST("/orders", createOrder)
    
    r.Run(":8080")
}

func createOrder(c *gin.Context) {
    var req struct {
        ProductID int `json:"product_id"`
        Quantity  int `json:"quantity"`
    }
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    // 创建订单
    orderID := createOrderRecord(req.ProductID, req.Quantity)
    
    // 复制 Context 用于异步处理
    cCp := c.Copy()
    
    // 异步发送通知
    go func() {
        userID := cCp.GetInt("userID")
        sendEmailNotification(userID, orderID)
    }()
    
    // 异步更新统计
    go func() {
        updateOrderStatistics(orderID)
    }()
    
    c.JSON(http.StatusOK, gin.H{
        "order_id": orderID,
        "message":  "order created",
    })
}

func createOrderRecord(productID, quantity int) int {
    return 1001 // 模拟返回订单 ID
}

func sendEmailNotification(userID, orderID int) {
    time.Sleep(100 * time.Millisecond) // 模拟耗时操作
    log.Printf("Email sent: user=%d, order=%d", userID, orderID)
}

func updateOrderStatistics(orderID int) {
    time.Sleep(50 * time.Millisecond)
    log.Printf("Statistics updated: order=%d", orderID)
}

小结

Context 复制是在 goroutine 中安全使用 Context 的关键:

  • 使用 c.Copy() 创建只读副本
  • 副本可以安全地在 goroutine 中使用
  • 副本不能用于写入响应
  • 注意 Request Body 只能读取一次
  • 在启动 goroutine 之前复制

正确使用 Context 复制,可以实现安全的异步处理,提升应用性能。