Contexto e Cancelamento
Cada handler do Gin recebe um *gin.Context, que envolve o context.Context padrão do Go junto com helpers de requisição e resposta. Entender como usar o contexto subjacente corretamente é essencial para construir aplicações de produção que lidam com timeouts, cancelamento e limpeza de recursos adequadamente.
Acessando o contexto da requisição
Seção intitulada “Acessando o contexto da requisição”O context.Context padrão para a requisição atual está disponível através de c.Request.Context(). Este é o contexto que você deve passar para qualquer chamada downstream — queries de banco de dados, requisições HTTP ou outras operações de I/O.
package main
import ( "log" "net/http"
"github.com/gin-gonic/gin")
func main() { r := gin.Default()
r.GET("/api/data", func(c *gin.Context) { ctx := c.Request.Context()
// Pass ctx to any downstream function that accepts context.Context. log.Println("request context deadline:", ctx.Done())
c.JSON(http.StatusOK, gin.H{"status": "ok"}) })
r.Run(":8080")}Timeouts de requisição
Seção intitulada “Timeouts de requisição”Você pode aplicar um timeout a requisições individuais usando um middleware. Quando o timeout expira, o contexto é cancelado e qualquer chamada downstream que respeite o cancelamento de contexto retornará imediatamente.
package main
import ( "context" "net/http" "time"
"github.com/gin-gonic/gin")
// TimeoutMiddleware wraps each request with a context deadline.func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel()
// Replace the request with one that carries the new context. c.Request = c.Request.WithContext(ctx) c.Next() }}
func main() { r := gin.Default() r.Use(TimeoutMiddleware(5 * time.Second))
r.GET("/api/slow", func(c *gin.Context) { ctx := c.Request.Context()
// Simulate work that respects the context deadline. select { case <-time.After(10 * time.Second): c.JSON(http.StatusOK, gin.H{"result": "done"}) case <-ctx.Done(): c.JSON(http.StatusGatewayTimeout, gin.H{ "error": "request timed out", }) } })
r.Run(":8080")}Passando contexto para queries de banco de dados
Seção intitulada “Passando contexto para queries de banco de dados”Drivers de banco de dados em Go aceitam um context.Context como primeiro argumento. Sempre passe o contexto da requisição para que queries sejam automaticamente canceladas se o cliente desconectar ou a requisição expirar.
package main
import ( "database/sql" "net/http"
"github.com/gin-gonic/gin" _ "github.com/lib/pq")
func main() { db, err := sql.Open("postgres", "postgres://localhost/mydb?sslmode=disable") if err != nil { panic(err) }
r := gin.Default()
r.GET("/api/users/:id", func(c *gin.Context) { ctx := c.Request.Context() id := c.Param("id")
var name string err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", id).Scan(&name) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return }
c.JSON(http.StatusOK, gin.H{"id": id, "name": name}) })
r.Run(":8080")}Passando contexto para chamadas HTTP de saída
Seção intitulada “Passando contexto para chamadas HTTP de saída”Quando seu handler chama serviços externos, passe o contexto da requisição para que chamadas de saída sejam canceladas junto com a requisição recebida.
package main
import ( "io" "net/http"
"github.com/gin-gonic/gin")
func main() { r := gin.Default()
r.GET("/api/proxy", func(c *gin.Context) { ctx := c.Request.Context()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://httpbin.org/delay/3", nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return }
resp, err := http.DefaultClient.Do(req) if err != nil { c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) return } defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body) c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), body) })
r.Run(":8080")}Tratando desconexão do cliente
Seção intitulada “Tratando desconexão do cliente”Quando um cliente fecha a conexão (por exemplo, navega para outra página ou cancela uma requisição), o contexto da requisição é cancelado. Você pode detectar isso em handlers de longa duração para parar o trabalho antecipadamente e liberar recursos.
package main
import ( "log" "net/http" "time"
"github.com/gin-gonic/gin")
func main() { r := gin.Default()
r.GET("/api/stream", func(c *gin.Context) { ctx := c.Request.Context()
for i := 0; ; i++ { select { case <-ctx.Done(): log.Println("client disconnected, stopping work") return case <-time.After(1 * time.Second): c.SSEvent("message", gin.H{"count": i}) c.Writer.Flush() } } })
r.Run(":8080")}Melhores práticas
Seção intitulada “Melhores práticas”-
Sempre propague o contexto da requisição. Passe
c.Request.Context()para toda função que aceite umcontext.Context— chamadas de banco de dados, clientes HTTP, chamadas gRPC e qualquer operação de I/O. Isso garante que cancelamento e timeouts se propaguem por toda a cadeia de chamadas. -
Não armazene
*gin.Contextem structs ou passe-o entre fronteiras de goroutines.gin.Contextestá vinculado ao ciclo de vida da requisição/resposta HTTP e não é seguro para uso concorrente. Em vez disso, extraia os valores que precisa (contexto da requisição, parâmetros, headers) antes de criar goroutines. -
Defina timeouts no nível do middleware. Um middleware de timeout oferece um único lugar para impor prazos em todas as rotas, em vez de duplicar a lógica de timeout em cada handler.
-
Use
context.WithValuecom moderação. Prefirac.Set()ec.Get()dentro dos handlers do Gin. Reservecontext.WithValuepara valores que precisam cruzar fronteiras de pacotes através de interfaces da biblioteca padrão.
Armadilhas comuns
Seção intitulada “Armadilhas comuns”Usando gin.Context em goroutines
Seção intitulada “Usando gin.Context em goroutines”gin.Context é reutilizado entre requisições para desempenho. Se você precisa acessá-lo de uma goroutine, deve chamar c.Copy() para criar uma cópia somente leitura. Usar o gin.Context original em uma goroutine leva a condições de corrida e comportamento imprevisível.
package main
import ( "log" "net/http" "time"
"github.com/gin-gonic/gin")
func main() { r := gin.Default()
r.GET("/api/async", func(c *gin.Context) { // WRONG: using c directly in a goroutine. // go func() { // log.Println(c.Request.URL.Path) // data race! // }()
// CORRECT: copy the context first. cCopy := c.Copy() go func() { time.Sleep(2 * time.Second) log.Printf("async work done for %s\n", cCopy.Request.URL.Path) }()
c.JSON(http.StatusOK, gin.H{"status": "processing"}) })
r.Run(":8080")}Ignorando cancelamento de contexto
Seção intitulada “Ignorando cancelamento de contexto”Se seu handler não verifica ctx.Done(), ele continuará executando mesmo depois que o cliente desconectou, desperdiçando CPU e memória. Sempre use APIs cientes de contexto (QueryRowContext, NewRequestWithContext, select em ctx.Done()) para que o trabalho pare assim que o contexto for cancelado.
Escrevendo a resposta após cancelamento do contexto
Seção intitulada “Escrevendo a resposta após cancelamento do contexto”Uma vez que o contexto é cancelado, evite escrever em c.Writer. A conexão pode já estar fechada, e escritas falharão silenciosamente ou causarão panic. Verifique ctx.Err() antes de escrever se seu handler realiza trabalho de longa duração.
func handler(c *gin.Context) { ctx := c.Request.Context()
result, err := doExpensiveWork(ctx) if err != nil { if ctx.Err() != nil { // Client is gone; do not write a response. return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return }
c.JSON(http.StatusOK, gin.H{"result": result})}