Context em Go: Cancelamento, Timeout e Propagação de Valores
O context é um dos pacotes mais importantes da biblioteca padrão de Go. Ele foi introduzido para resolver um problema crítico em aplicações concorrentes: como coordenar o ciclo de vida de operações e garantir que goroutines sejam finalizadas de forma segura e previsível. Sem o context, você teria que usar canais manuais, sinais de cancelamento complexos ou variáveis compartilhadas — tudo aumentando a complexidade e o risco de deadlocks.
Quando você trabalha com requisições HTTP, processamento de dados assíncrono, ou qualquer operação que envolva múltiplas goroutines, o context atua como um "fio condutor" que passa entre as funções, carregando informações sobre cancelamento, timeouts e valores específicos da operação. Entender context é essencial para escrever código Go robusto e profissional.
O Que é Context e Por Que Usar
A Essência do Context
Um context.Context é uma interface que encapsula um sinal de cancelamento, um deadline (prazo de expiração) e valores de dados imutáveis. A interface é simples, mas poderosa:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
O método Done() retorna um canal que será fechado quando o contexto for cancelado ou expirado. O método Err() te diz por que o contexto foi cancelado — se foi por timeout (context.DeadlineExceeded) ou por cancelamento explícito (context.Canceled). O método Value() permite recuperar dados armazenados no contexto.
O ponto-chave é que context é imutável e seguro para usar em múltiplas goroutines simultaneamente. Quando você precisa de um novo contexto com diferentes propriedades (como um timeout mais curto), você não modifica o existente — você cria um novo derivado.
O Padrão de Design
A prática recomendada é sempre passar context.Context como primeiro argumento em funções que realizam operações potencialmente bloqueantes. Isso torna explícito que a função respeita cancelamento e timeouts. Veja um exemplo clássico:
// Padrão correto: context como primeiro argumento
func FetchUserData(ctx context.Context, userID string) (*User, error) {
// implementação que respeita ctx
}
// Padrão incorreto: context em outro lugar
func FetchUserData(userID string, ctx context.Context) (*User, error) {
// dificulta leitura e é contrário à convenção Go
}
Cancelamento de Operações
Entendendo WithCancel
O context.WithCancel() retorna um contexto derivado que você pode cancelar manualmente chamando uma função de cancelamento. Isso é útil quando você quer parar uma operação de longa duração sem aguardar um timeout.
package main
import (
"context"
"fmt"
"time"
)
func Worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d: Cancelado. Razão: %v\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d: Trabalhando...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
// Cria um contexto que pode ser cancelado
ctx, cancel := context.WithCancel(context.Background())
// Inicia 3 workers
for i := 1; i <= 3; i++ {
go Worker(ctx, i)
}
// Deixa os workers rodar por 2 segundos
time.Sleep(2 * time.Second)
// Cancela todos os workers de uma vez
fmt.Println("Cancelando todos os workers...")
cancel()
// Aguarda um pouco para ver as mensagens
time.Sleep(1 * time.Second)
}
Quando você chama cancel(), o canal Done() é fechado, e todas as goroutines que estão esperando naquele contexto são despertadas. O método Err() retorna context.Canceled. Isso permite interromper operações que estão em loop ou aguardando I/O de forma elegante.
Padrão Prático de Cancelamento
Em aplicações reais, frequentemente você quer respeitar cancelamento externo (como quando um cliente desconecta de uma requisição HTTP) e também impor um timeout próprio. Aqui está como combinar essas ideias:
package main
import (
"context"
"fmt"
"time"
)
func ProcessRequest(ctx context.Context, clientName string) error {
// Cria um novo contexto derivado com timeout de 3 segundos
// mas que ainda pode ser cancelado pelo ctx pai
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
select {
case <-time.After(5 * time.Second):
// Simula uma operação que leva 5 segundos
fmt.Printf("%s: Operação concluída\n", clientName)
return nil
case <-ctx.Done():
// Timeout ou cancelamento externo
return fmt.Errorf("%s: %w", clientName, ctx.Err())
}
}
func main() {
// Contexto raiz
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Inicia 2 requisições
go func() {
err := ProcessRequest(ctx, "Cliente A")
fmt.Println("Cliente A:", err)
}()
go func() {
err := ProcessRequest(ctx, "Cliente B")
fmt.Println("Cliente B:", err)
}()
time.Sleep(6 * time.Second)
}
Neste exemplo, cada requisição tem seu próprio timeout de 3 segundos, mas ambas podem ser canceladas juntas se você chamar cancel() no contexto pai.
Timeouts e Deadlines
WithTimeout e WithDeadline
Go oferece duas funções similares: WithTimeout() que recebe uma duração, e WithDeadline() que recebe um tempo absoluto. WithTimeout() é mais comum e prático porque você normalmente pensa em termos de "quanto tempo este deve levar?" em vez de "qual é a hora absoluta de expiração?".
package main
import (
"context"
"fmt"
"time"
)
func SlowAPI(ctx context.Context) (string, error) {
// Simula uma API lenta que leva 10 segundos
select {
case <-time.After(10 * time.Second):
return "Sucesso", nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
// Exemplo 1: Timeout curto (vai expirar)
fmt.Println("=== Teste com Timeout de 2 segundos ===")
ctx1, cancel1 := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel1()
start := time.Now()
result, err := SlowAPI(ctx1)
duration := time.Since(start)
if err != nil {
fmt.Printf("Erro: %v (após %.1f segundos)\n", err, duration.Seconds())
} else {
fmt.Printf("Resultado: %s\n", result)
}
// Exemplo 2: Timeout longo (vai completar)
fmt.Println("\n=== Teste com Timeout de 15 segundos ===")
ctx2, cancel2 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel2()
start = time.Now()
result, err = SlowAPI(ctx2)
duration = time.Since(start)
if err != nil {
fmt.Printf("Erro: %v\n", err)
} else {
fmt.Printf("Resultado: %s (após %.1f segundos)\n", result, duration.Seconds())
}
}
O timing é preciso: quando o contexto expira, ctx.Done() é fechado e ctx.Err() retorna context.DeadlineExceeded. Isso permite que suas funções respondam quase imediatamente, sem precisar de timers adicionais.
Usando Deadlines Absolutos
Embora menos comum, WithDeadline() é útil quando você tem um timestamp específico até o qual uma operação deve ser concluída:
package main
import (
"context"
"fmt"
"time"
)
func ProcessWithDeadline() {
// Define um deadline absoluto: 5 segundos a partir de agora
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
// Tenta uma operação que leva 3 segundos
select {
case <-time.After(3 * time.Second):
fmt.Println("Operação concluída antes do deadline")
case <-ctx.Done():
fmt.Printf("Deadline excedido: %v\n", ctx.Err())
}
}
func main() {
ProcessWithDeadline()
}
Na prática, WithTimeout() é mais legível e é o que você usará 99% das vezes.
Propagação de Valores através do Context
Value() e WithValue()
O context também funciona como um "saco" imutável de valores. Você pode armazenar dados que precisam ser acessados por múltiplas funções na call stack sem passá-los como parâmetros. Isso é especialmente útil para metadados como IDs de requisição, usuário autenticado, ou configurações específicas.
package main
import (
"context"
"fmt"
)
// Define tipos de chave para evitar colisões
type ContextKey string
const (
RequestIDKey ContextKey = "request_id"
UserIDKey ContextKey = "user_id"
UserNameKey ContextKey = "user_name"
)
func GetUserInfo(ctx context.Context) {
// Recupera valores do contexto
requestID := ctx.Value(RequestIDKey)
userID := ctx.Value(UserIDKey)
userName := ctx.Value(UserNameKey)
fmt.Printf("RequestID: %v, UserID: %v, UserName: %v\n",
requestID, userID, userName)
}
func FetchUserDetails(ctx context.Context) {
// A função recebe o contexto já populado
GetUserInfo(ctx)
}
func HandleRequest(ctx context.Context) {
// Adiciona valores ao contexto
ctx = context.WithValue(ctx, RequestIDKey, "req-12345")
ctx = context.WithValue(ctx, UserIDKey, 42)
ctx = context.WithValue(ctx, UserNameKey, "Alice")
// Passa para outras funções
FetchUserDetails(ctx)
}
func main() {
ctx := context.Background()
HandleRequest(ctx)
}
Perceba que WithValue() retorna um novo contexto — nunca modifica o existente. Você pode encadear múltiplas chamadas WithValue() para montar o contexto que precisa. Os valores são recuperados por chave usando Value(), que retorna interface{}, então você precisará fazer type assertion se necessário.
Boas Práticas com Values
Existem algumas regras importantes ao trabalhar com valores em context:
-
Use tipos customizados para chaves — Não use strings diretamente para chaves, pois há risco de colisão. Crie um tipo customizado (como
type ContextKey string) em cada package que vai usá-lo. -
Valores devem ser imutáveis — Não armazene slices ou maps mutáveis, pois isso viola a natureza segura do context. Se precisar de dados compartilhados mutáveis, use mutex e canais.
-
Não abuse de values — Use context.Values para metadados da requisição (IDs, usuário autenticado), não para dados de negócio. Se está passando muitos valores, considere usar uma struct como argumento da função.
package main
import (
"context"
"fmt"
)
type ContextKey string
const RequestIDKey ContextKey = "request_id"
// Exemplo de má prática
func BadExample(ctx context.Context) {
// ❌ Armazenar slice mutável é perigoso
ctx = context.WithValue(ctx, "items", []string{"a", "b"})
}
// Exemplo correto
func GoodExample(ctx context.Context) {
// ✓ Armazenar valor imutável
ctx = context.WithValue(ctx, RequestIDKey, "req-42")
// Recuperar e usar
if id := ctx.Value(RequestIDKey); id != nil {
fmt.Printf("Request ID: %s\n", id.(string))
}
}
func main() {
GoodExample(context.Background())
}
Integrando Context em Aplicações Reais
Example: HTTP Server com Context
Um caso de uso prático é integrar context em handlers HTTP para respeitar timeouts de cliente e cancelamento:
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
)
type ContextKey string
const RequestIDKey ContextKey = "request_id"
func SlowEndpoint(w http.ResponseWriter, r *http.Request) {
// O *http.Request já traz um context
ctx := r.Context()
// Adiciona um timeout adicional para esta operação específica
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// Simula processamento longo
select {
case <-time.After(3 * time.Second):
fmt.Fprintf(w, "Sucesso! RequestID: %v\n", ctx.Value(RequestIDKey))
case <-ctx.Done():
w.WriteHeader(http.StatusRequestTimeout)
fmt.Fprintf(w, "Requisição expirou: %v\n", ctx.Err())
}
}
func MiddlewareRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Adiciona um ID único para rastreamento
ctx := context.WithValue(r.Context(), RequestIDKey, fmt.Sprintf("req-%d", time.Now().UnixNano()))
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
func main() {
// Cria um servidor com timeout de leitura
mux := http.NewServeMux()
mux.HandleFunc("/slow", SlowEndpoint)
handler := MiddlewareRequestID(mux)
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
}
log.Println("Iniciando servidor em :8080")
log.Fatal(server.ListenAndServe())
}
Este exemplo mostra como context flui naturalmente em uma aplicação web real. O middleware adiciona um ID de requisição, os handlers respeitam timeouts, e tudo é limpo automaticamente quando a requisição termina.
Example: Pool de Workers com Context
Outro padrão comum é usar context para coordenar múltiplos workers que processam tarefas:
package main
import (
"context"
"fmt"
"sync"
"time"
)
func Worker(ctx context.Context, id int, jobs <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case job, ok := <-jobs:
if !ok {
fmt.Printf("Worker %d: Canal fechado\n", id)
return
}
fmt.Printf("Worker %d: Processando job %d\n", id, job)
time.Sleep(500 * time.Millisecond)
case <-ctx.Done():
fmt.Printf("Worker %d: Contexto cancelado - %v\n", id, ctx.Err())
return
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
jobs := make(chan int, 10)
var wg sync.WaitGroup
// Inicia 3 workers
for i := 1; i <= 3; i++ {
wg.Add(1)
go Worker(ctx, i, jobs, &wg)
}
// Envia jobs
go func() {
for j := 1; j <= 10; j++ {
select {
case jobs <- j:
fmt.Printf("Job %d enviado\n", j)
case <-ctx.Done():
fmt.Println("Context expirou, parando de enviar jobs")
return
}
}
close(jobs)
}()
// Aguarda conclusão
wg.Wait()
fmt.Println("Todos os workers finalizados")
}
Aqui, o timeout no contexto para todos os workers simultaneamente quando expira. Isso evita que threads fiquem presas ou que você tenha que coordenar cancelamento manualmente.
Conclusão
Aprendemos que o context é o mecanismo padrão em Go para coordenar o ciclo de vida de operações concorrentes. Em primeiro lugar, o context permite cancelamento elegante e timeouts precisos através de WithCancel(), WithTimeout() e WithDeadline() — eliminando a necessidade de lógica manual com canais para cada operação. Em segundo lugar, o context propaga valores imutáveis (como IDs de requisição e usuário autenticado) através da call stack de forma segura e eficiente, sem necessidade de passar argumentos extras. Por fim, o padrão de sempre passar context como primeiro argumento de função tornou-se uma convenção Go tão forte que violá-la marca código de baixa qualidade — então sempre respeite esse padrão em suas aplicações.