GinSeniorTechnical

Как goroutine взаимодействуют с Context Gin — почему не следует передавать *gin.Context в goroutine?

*gin.Context небезопасен для goroutine: после завершения хендлера Gin возвращает его в sync.Pool. Передавайте в goroutine только скопированные значения или c.Copy(), а для отмены — context.Context из c.Request.Context().

Почему *gin.Context нельзя передавать в goroutine

*gin.Context — не потокобезопасная структура. Gin возвращает объект контекста в пул (sync.Pool) сразу после завершения цепочки обработчиков. Если goroutine получает указатель на контекст и обращается к нему после того, как основной хендлер вернул управление, она работает с уже «мусорным» или повторно используемым объектом. Это приводит к гонкам данных и непредсказуемому поведению.

Что происходит под капотом

Gin хранит пул контекстов в Engine.pool *sync.Pool. После возврата хендлера вызывается engine.pool.Put(c). При следующем запросе тот же объект достаётся из пула и переписывается. Goroutine, запущенная в хендлере, может читать уже чужие данные или писать в чужой ResponseWriter.

Правильный паттерн: копировать нужные данные до запуска goroutine

func HandleOrder(c *gin.Context) {
	// Извлекаем всё нужное до запуска goroutine
	userID := c.GetString("user_id")
	var req OrderRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}

	// Копируем контекст запроса (стандартный context.Context, НЕ *gin.Context)
	ctx := c.Request.Context()

	go func(ctx context.Context, uid string, order OrderRequest) {
		if err := processOrder(ctx, uid, order); err != nil {
			log.Printf("order processing failed: %v", err)
		}
	}(ctx, userID, req)

	c.JSON(202, gin.H{"status": "accepted"})
}

Использование c.Copy() для передачи копии контекста

Gin предоставляет метод c.Copy(), который создаёт снимок gin.Context с отвязанным writermem. Копия безопасна для чтения в goroutine, но запись в ResponseWriter через неё невозможна (что правильно).

func HandleNotification(c *gin.Context) {
	cCopy := c.Copy() // создаём безопасную копию

	go func() {
		// Читаем из копии: заголовки, params, keys
		token := cCopy.GetHeader("Authorization")
		platform := cCopy.Query("platform")
		sendPushNotification(token, platform)
	}()

	c.JSON(200, gin.H{"ok": true})
}

Работа с context.Context внутри goroutine

Для отмены и таймаутов передавайте context.Context из запроса, а не весь gin.Context:

func HandleExport(c *gin.Context) {
	// context.Context с дедлайном
	ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
	defer cancel()

	resultCh := make(chan []byte, 1)
	errCh := make(chan error, 1)

	go func() {
		data, err := generateReport(ctx)
		if err != nil {
			errCh <- err
			return
		}
		resultCh <- data
	}()

	select {
	case data := <-resultCh:
		c.Data(200, "application/pdf", data)
	case err := <-errCh:
		c.JSON(500, gin.H{"error": err.Error()})
	case <-ctx.Done():
		c.JSON(504, gin.H{"error": "timeout"})
	}
}

Подводные камни

  • c.Copy() не копирует тело запроса. Request.Body — это io.ReadCloser, читается один раз. Если хендлер уже прочитал тело (через ShouldBindJSON), goroutine получит пустой Body.
  • Запись в ResponseWriter из goroutine — data race. Даже через c.Copy() ResponseWriter заблокирован; попытка записать ответ в goroutine после завершения хендлера завершится паникой или гонкой.
  • context.Context из Request может быть отменён. Когда клиент закрыл соединение, c.Request.Context() отменяется. Если goroutine должна продолжить работу независимо, передавайте context.Background() или специальный сервисный контекст.
  • sync.Pool и GC. Объект из пула может быть собран GC, если пул не удерживает ссылку. Никогда не кешируйте *gin.Context в глобальных переменных.
  • Goroutine leak при долгих задачах. Запускайте фоновые задачи через управляемый worker pool, а не «голые» goroutine — иначе при всплеске нагрузки будет тысячи goroutine без ограничения.
  • Логирование через c после завершения хендлера. Вызов c.Get("logger") из goroutine может вернуть нулевое значение или значение другого запроса.

Common mistakes

  • Давать ответ про goroutine и *gin.Context только на уровне определения, не показывая поведение в реальном приложении.
  • Игнорировать границы ответственности вокруг темы «goroutine и *gin.Context»: кто отменяет работу, кто владеет ресурсом и где формируется ответ клиенту.
  • Не связывать goroutine и *gin.Context с observability, тестированием или безопасностью, когда это влияет на продакшен-поведение.

What the interviewer is testing

  • Точно объясняет, что именно делает goroutine и *gin.Context и где это используется в Go-коде.
  • Связывает goroutine и *gin.Context с корректным lifecycle запроса, отменой, конкурентностью или конфигурацией сервера там, где это уместно.
  • Не изобретает API и опирается на реальные контракты официальной документации.

Sources

Related topics