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})
}()
}
上面的代码可能导致数据竞争和不可预期的行为。
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 方法会创建一个新的 Context,包含以下内容的副本:
但注意:复制后的 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})
}
func handler(c *gin.Context) {
cCp := c.Copy()
go func() {
// 这不会生效!副本不能写入响应
cCp.JSON(200, gin.H{"message": "async"})
}()
c.JSON(200, gin.H{"message": "sync"})
}
func handler(c *gin.Context) {
// 读取 body
body, _ := c.GetRawData()
cCp := c.Copy()
go func() {
// 副本中的 body 已经被读取过了
// 需要在复制前保存需要的数据
processBody(body)
}()
}
在启动 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() 创建只读副本正确使用 Context 复制,可以实现安全的异步处理,提升应用性能。