Как 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 и опирается на реальные контракты официальной документации.