Context: propagando cancelamento e deadlines
1. Introdução ao pacote context
Antes da introdução do pacote context na biblioteca padrão do Go, o gerenciamento de concorrência era feito manualmente com canais e variáveis compartilhadas. Isso gerava código propenso a vazamentos de goroutines e difícil de manter. Por exemplo, para cancelar uma operação concorrente, era comum usar um canal done como sinalizador:
func operacaoLonga(done <-chan struct{}) {
select {
case <-done:
return
case <-time.After(5 * time.Second):
// processamento
}
}
O pacote context padronizou essa comunicação, fornecendo uma interface clara para propagar cancelamentos, deadlines e valores entre goroutines. A interface context.Context define quatro métodos essenciais:
Done(): retorna um canal que é fechado quando o contexto é cancelado ou expiraDeadline(): retorna o deadline absoluto, se existirErr(): retorna o motivo do cancelamento (CanceledouDeadlineExceeded)Value(): recupera valores associados ao contexto
2. Criando contextos pai e filho
Todo contexto em Go deriva de um contexto raiz. As funções context.Background() e context.TODO() criam esses contextos iniciais:
ctx := context.Background() // contexto vazio, nunca cancelado
ctx2 := context.TODO() // placeholder para quando o contexto ainda não está definido
A hierarquia de contextos permite criar contextos filhos que herdam o comportamento do pai. Quando um contexto pai é cancelado, todos os seus descendentes também são cancelados automaticamente:
func main() {
pai, cancel := context.WithCancel(context.Background())
filho := context.WithValue(pai, "user", "admin")
cancel() // cancela pai e também filho
fmt.Println(filho.Err()) // imprime "context canceled"
}
Use Background() para o contexto raiz da aplicação e TODO() apenas temporariamente durante o desenvolvimento, até definir o contexto adequado.
3. Cancelamento explícito com context.WithCancel
A função context.WithCancel recebe um contexto pai e retorna um novo contexto filho e uma função cancel. Quando cancel é chamada, o canal Done() do contexto filho é fechado, sinalizando todas as goroutines que escutam esse canal:
func processarDados(ctx context.Context, dados []int) <-chan int {
resultados := make(chan int)
go func() {
defer close(resultados)
for _, d := range dados {
select {
case <-ctx.Done():
fmt.Println("Processamento cancelado:", ctx.Err())
return
case resultados <- d * 2:
time.Sleep(100 * time.Millisecond)
}
}
}()
return resultados
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
dados := []int{1, 2, 3, 4, 5}
ch := processarDados(ctx, dados)
// Cancela após receber 3 resultados
for i := 0; i < 3; i++ {
fmt.Println(<-ch)
}
cancel() // propaga cancelamento para a goroutine
}
A propagação em cascata é automática: se o pai for cancelado, todos os filhos também são, independentemente de onde o cancelamento ocorre.
4. Deadlines e timeouts com context.WithDeadline e context.WithTimeout
WithDeadline define um momento absoluto para o cancelamento, enquanto WithTimeout é um atalho que recebe uma duração relativa:
// Deadline absoluto: 2 segundos a partir de agora
deadline := time.Now().Add(2 * time.Second)
ctxDeadline, cancelDeadline := context.WithDeadline(context.Background(), deadline)
defer cancelDeadline()
// Timeout relativo: mesmo efeito, mas mais conciso
ctxTimeout, cancelTimeout := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelTimeout()
Em chamadas de rede, timeouts são essenciais para evitar bloqueios eternos:
func buscarDadosAPI(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
dados, err := buscarDadosAPI(ctx, "https://api.exemplo.com/dados")
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Timeout excedido")
} else {
fmt.Println("Erro:", err)
}
return
}
fmt.Println("Dados recebidos:", len(dados))
}
É importante distinguir os erros: DeadlineExceeded indica que o tempo expirou, enquanto Canceled indica cancelamento explícito.
5. Propagação de cancelamento em chamadas encadeadas
O contexto deve ser passado explicitamente entre funções e serviços para garantir que cancelamentos se propaguem corretamente. Em pipelines de goroutines, cada estágio deve respeitar o contexto:
func etapa1(ctx context.Context, entrada <-chan int) <-chan int {
saida := make(chan int)
go func() {
defer close(saida)
for v := range entrada {
select {
case <-ctx.Done():
return
case saida <- v * 2:
}
}
}()
return saida
}
func etapa2(ctx context.Context, entrada <-chan int) <-chan int {
saida := make(chan int)
go func() {
defer close(saida)
for v := range entrada {
select {
case <-ctx.Done():
return
case saida <- v + 1:
}
}
}()
return saida
}
Em servidores HTTP, o contexto da requisição já carrega informações de cancelamento. Se o cliente desconectar, o contexto é cancelado automaticamente:
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
resultado, err := operacaoLonga(ctx)
if err != nil {
if errors.Is(err, context.Canceled) {
return // cliente desconectou, não precisa responder
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintln(w, resultado)
}
6. Valores no contexto: context.WithValue
WithValue permite associar pares chave-valor ao contexto, úteis para propagar informações como IDs de requisição, tokens de autenticação ou metadados:
type contextKey string
const (
keyRequestID contextKey = "request_id"
keyUserID contextKey = "user_id"
)
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), keyRequestID, uuid.New().String())
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func handler(w http.ResponseWriter, r *http.Request) {
requestID := r.Context().Value(keyRequestID).(string)
fmt.Println("Processando requisição:", requestID)
}
Evite usar valores no contexto para passar parâmetros opcionais de funções ou dados mutáveis. O contexto deve ser imutável e os valores devem ser usados apenas para metadados de escopo amplo.
7. Boas práticas e armadilhas comuns
Nunca armazene contextos em structs — sempre passe como primeiro parâmetro das funções:
// ❌ Ruim: contexto armazenado em struct
type Service struct {
ctx context.Context
}
// ✅ Bom: contexto passado como parâmetro
func (s *Service) Processar(ctx context.Context, dados []byte) error {
// ...
}
Verifique ctx.Err() antes de operações longas para evitar trabalho desnecessário:
func operacaoPesada(ctx context.Context) error {
if err := ctx.Err(); err != nil {
return err
}
// operação longa aqui
}
Testando cancelamento em testes unitários:
func TestOperacaoComCancelamento(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // cancela imediatamente
err := operacaoPesada(ctx)
if !errors.Is(err, context.Canceled) {
t.Errorf("esperava cancelamento, obteve: %v", err)
}
}
Lembre-se de sempre chamar a função cancel para liberar recursos, mesmo que o contexto não seja cancelado explicitamente. Use defer cancel() imediatamente após criar o contexto.
Referências
- Package context - Go Documentation — Documentação oficial do pacote context na biblioteca padrão do Go
- Go Concurrency Patterns: Context — Artigo oficial do Go Blog sobre padrões de concorrência com context
- Using Go Context - Dave Cheney — Explicação detalhada sobre cancelamento com context por Dave Cheney
- Context in Go: Understanding and Using Context - DigitalOcean — Tutorial prático sobre uso de context em Go
- Go Context Patterns - Just For Func (YouTube) — Vídeo-tutorial sobre patterns avançados de context em Go
- Context Cancellation and Timeouts - The Go Way — Artigo sobre cancelamento e timeouts com context no Medium