---
title: "GN010 – Do Not Store gin.Context in Goroutines Beyond the Handler"
impact: high
impactDescription: "gin.Context is recycled after the handler returns; storing it in a goroutine creates a data race on pool-recycled memory."
tags: [go, gin, concurrency, correctness]
---

# GN010 – Do Not Store `gin.Context` in Goroutines Beyond the Handler

## Rule

Never pass `*gin.Context` to a goroutine that outlives the handler function. Copy the values you need (request data, context) before spawning the goroutine.

## Why

Gin uses `sync.Pool` to reuse `*gin.Context` instances for performance. Once the handler function returns, Gin resets and recycles the context object. A goroutine holding a reference to the old `*gin.Context` now reads from a recycled object used by a completely different request — this is an undetected data race and a source of subtle, hard-to-reproduce bugs.

## Wrong

```go
func (h *NotificationHandler) Send(c *gin.Context) {
    var req SendNotificationRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // ❌ goroutine holds *gin.Context — races after handler returns
    go func() {
        userID := c.GetString("user_id")   // c is already recycled here
        h.service.Notify(context.Background(), userID, req)
    }()

    c.JSON(http.StatusAccepted, gin.H{"status": "queued"})
}
```

## Correct

```go
func (h *NotificationHandler) Send(c *gin.Context) {
    var req SendNotificationRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // ✅ Copy values from context BEFORE spawning the goroutine
    userID := c.GetString("user_id")   // copy string value
    ctx := c.Request.Context()         // copy the stdlib context (safe to pass to goroutine)

    go func() {
        // Use copied values — no reference to *gin.Context
        if err := h.service.Notify(ctx, userID, req); err != nil {
            h.logger.Error("notification failed", slog.String("user_id", userID), slog.Any("error", err))
        }
    }()

    c.JSON(http.StatusAccepted, gin.H{"status": "queued"})
}
```

## Notes

- `c.Request.Context()` is a standard `context.Context` backed by the HTTP request — safe to copy and pass to goroutines.
- Extract all required values (`c.Param()`, `c.GetHeader()`, `c.Get()`) into local variables before the goroutine.
- The Go race detector (`go test -race`) will catch this violation in tests if the goroutine runs fast enough — enable it in CI.
- Prefer `c.Copy()` if you absolutely need a snapshot of the whole context, but this is memory-heavier than copying specific fields.
