Context 并发安全

在处理请求时,有时需要启动 goroutine 进行异步处理。Context 不是并发安全的,需要特殊处理。

Context 不是并发安全的

直接在 goroutine 中使用 Context 会有问题:

r.GET("/unsafe", func(c *gin.Context) {
    go func() {
        c.JSON(200, gin.H{"message": "这样做不安全"})
    }()
})

这样做可能导致竞态条件,因为多个 goroutine 同时访问 Context。

使用 Copy 方法

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

package main

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

func main() {
    r := gin.Default()
    
    r.GET("/async", func(c *gin.Context) {
        cCp := c.Copy()
        
        go func() {
            time.Sleep(time.Second)
            
            println("异步处理完成: " + cCp.Request.URL.Path)
        }()
        
        c.String(http.StatusOK, "请求已接收,正在后台处理")
    })
    
    r.Run(":8080")
}

异步日志记录

func AsyncLogger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        
        c.Next()
        
        cCp := c.Copy()
        go func() {
            log.Printf("[%s] %s %s - %d - %v",
                time.Now().Format("2006-01-02 15:04:05"),
                cCp.Request.Method,
                cCp.Request.URL.Path,
                cCp.Writer.Status(),
                time.Since(start),
            )
        }()
    }
}

r.Use(AsyncLogger())

异步发送通知

func NotifyOnComplete(c *gin.Context, message string) {
    cCp := c.Copy()
    go func() {
        requestID, _ := cCp.Get("requestID")
        user, _ := cCp.Get("currentUser")
        
        sendNotification(requestID.(string), user.(*User).Email, message)
    }()
}

r.POST("/order", Auth(), func(c *gin.Context) {
    var order Order
    c.ShouldBindJSON(&order)
    
    createOrder(order)
    
    NotifyOnComplete(c, "订单创建成功")
    
    c.JSON(200, gin.H{"message": "订单已创建"})
})

异步写入数据库

func AsyncSaveAnalytics(c *gin.Context, data map[string]interface{}) {
    cCp := c.Copy()
    go func() {
        data["requestID"], _ = cCp.Get("requestID")
        data["clientIP"] = cCp.ClientIP()
        data["userAgent"] = cCp.UserAgent()
        data["timestamp"] = time.Now()
        
        saveToDatabase(data)
    }()
}

r.GET("/track", func(c *gin.Context) {
    AsyncSaveAnalytics(c, map[string]interface{}{
        "action": "page_view",
        "page":   c.Query("page"),
    })
    
    c.String(200, "OK")
})

使用 channel 处理结果

r.GET("/process", func(c *gin.Context) {
    resultChan := make(chan string)
    
    cCp := c.Copy()
    go func() {
        time.Sleep(time.Second)
        resultChan <- "处理完成: " + cCp.Request.URL.Path
    }()
    
    select {
    case result := <-resultChan:
        c.String(200, result)
    case <-time.After(2 * time.Second):
        c.String(408, "处理超时")
    }
})

并发处理多个任务

func fetchUserData(userID int) interface{} {
    time.Sleep(100 * time.Millisecond)
    return map[string]interface{}{"id": userID, "name": "用户" + string(rune(userID))}
}

func fetchUserOrders(userID int) interface{} {
    time.Sleep(150 * time.Millisecond)
    return []string{"订单1", "订单2"}
}

r.GET("/user/:id/detail", func(c *gin.Context) {
    userID := c.Param("id")
    
    var wg sync.WaitGroup
    userData := make(chan interface{}, 1)
    orderData := make(chan interface{}, 1)
    
    cCp := c.Copy()
    
    wg.Add(2)
    
    go func() {
        defer wg.Done()
        userData <- fetchUserData(userID)
    }()
    
    go func() {
        defer wg.Done()
        orderData <- fetchUserOrders(userID)
    }()
    
    go func() {
        wg.Wait()
        close(userData)
        close(orderData)
    }()
    
    c.JSON(200, gin.H{
        "user":   <-userData,
        "orders": <-orderData,
        "path":   cCp.Request.URL.Path,
    })
})

使用 context.Context

Go 标准库的 context.Context 用于控制 goroutine 生命周期:

r.GET("/with-timeout", func(c *gin.Context) {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    
    cCp := c.Copy()
    resultChan := make(chan string, 1)
    
    go func() {
        time.Sleep(time.Second)
        resultChan <- "处理完成: " + cCp.Request.URL.Path
    }()
    
    select {
    case result := <-resultChan:
        c.String(200, result)
    case <-ctx.Done():
        c.String(408, "请求超时")
    }
})

注意事项

  1. 只在 goroutine 中使用 c.Copy() 的返回值
  2. 不要在 goroutine 中修改原始 Context
  3. 注意 goroutine 泄漏,确保能正常结束
  4. 使用 context.Context 控制超时

小结

Context 本身不是并发安全的,在 goroutine 中使用需要调用 Copy() 创建副本。异步处理时注意资源管理和错误处理,避免 goroutine 泄漏。结合标准库的 context.Context 可以更好地控制 goroutine 生命周期。