O Pacote context do Go em 2026: Cancelamento, Timeouts e Perguntas de Entrevista

Guia completo sobre o pacote context do Go: gerenciamento de cancelamento, timeouts e deadlines. Inclui perguntas frequentes de entrevistas técnicas e melhores práticas de produção.

O pacote context do Go - Cancelamento e Timeouts

O pacote context fornece o mecanismo padrão em Go para transportar deadlines, sinais de cancelamento e valores associados a requisições através das fronteiras de API. Cada aplicação Go em produção utiliza context: handlers HTTP, consultas ao banco de dados, chamadas gRPC e workers em segundo plano dependem deste pacote para o encerramento gracioso e gerenciamento de timeouts.

Essencial em Entrevistas

O context representa um dos tópicos mais frequentemente abordados em entrevistas de Go. Os entrevistadores esperam que os candidatos expliquem a propagação do context, demonstrem o tratamento correto do cancelamento e evitem armadilhas comuns como armazenar o context em structs.

A Interface Context e seus Quatro Métodos

A interface Context define quatro métodos que toda implementação deve satisfazer. Compreender esses métodos constitui a base para o uso efetivo do context.

context_interface.gogo
type Context interface {
    // Deadline returns the time when work should be canceled
    Deadline() (deadline time.Time, ok bool)
    
    // Done returns a channel that closes when the context is canceled
    Done() <-chan struct{}
    
    // Err returns the reason why Done was closed
    Err() error
    
    // Value returns the value associated with key, or nil
    Value(key any) any
}

Done() retorna um channel somente leitura. Quando este channel fecha, qualquer goroutine ouvindo-o recebe o sinal imediatamente. O padrão <-ctx.Done() bloqueia até que o cancelamento ocorra. Err() explica o motivo: seja context.Canceled quando cancelado explicitamente, ou context.DeadlineExceeded quando um timeout ou deadline expirou.

Criação de Contexts com Background e TODO

Duas funções criam contexts raiz: context.Background() e context.TODO(). Ambas retornam contexts não nulos e vazios que nunca são cancelados.

context_creation.gogo
package main

import (
    "context"
    "log"
    "net/http"
)

func main() {
    // Background: the root context for your application
    ctx := context.Background()
    
    // Use it as parent for derived contexts
    server := &http.Server{Addr: ":8080"}
    
    // TODO: placeholder when context source is unclear
    // Static analysis tools can flag context.TODO() for review
    processLegacyData(context.TODO())
}

func processLegacyData(ctx context.Context) {
    // Context parameter enables future cancellation support
    log.Println("Processing data...")
}

Background() serve como pai para requisições entrantes, funções main e código de inicialização. TODO() atua como um placeholder durante refatoração quando a fonte correta do context ainda não está determinada. Ferramentas de análise estática podem sinalizar o uso de TODO() para acompanhamento posterior.

Cancelamento com WithCancel e WithCancelCause

O cancelamento manual permite que goroutines pais sinalizem às filhas que o trabalho deve parar. Este padrão aparece em pools de workers, tarefas em segundo plano e implementações de encerramento gracioso.

cancellation.gogo
package main

import (
    "context"
    "errors"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int, results chan<- int) {
    for {
        select {
        case <-ctx.Done():
            // Check why cancellation occurred
            if cause := context.Cause(ctx); cause != nil {
                fmt.Printf("Worker %d stopped: %v\n", id, cause)
            }
            return
        default:
            // Simulate work
            time.Sleep(100 * time.Millisecond)
            results <- id * 10
        }
    }
}

func main() {
    // WithCancelCause provides error context
    ctx, cancel := context.WithCancelCause(context.Background())
    results := make(chan int, 10)
    
    // Start 3 workers
    for i := 1; i <= 3; i++ {
        go worker(ctx, i, results)
    }
    
    // Collect some results
    for i := 0; i < 5; i++ {
        fmt.Println("Result:", <-results)
    }
    
    // Cancel with a specific reason
    cancel(errors.New("shutdown requested by user"))
    
    // context.Cause retrieves the cancellation reason
    time.Sleep(50 * time.Millisecond)
    fmt.Println("Cause:", context.Cause(ctx))
}

WithCancelCause, introduzido no Go 1.20, fornece informações de erro mais ricas do que o simples WithCancel. A função context.Cause() recupera o erro passado para a função cancel, facilitando a depuração em sistemas complexos.

Pronto para mandar bem nas entrevistas de Go?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Timeouts e Deadlines para Processamento de Requisições

Handlers HTTP, chamadas ao banco de dados e requisições a APIs externas devem sempre ter timeouts. O pacote context oferece WithTimeout e WithDeadline para este propósito. Ambos criam contexts que se cancelam automaticamente quando o tempo expira.

timeouts.gogo
package main

import (
    "context"
    "fmt"
    "time"
)

// fetchFromAPI simulates an HTTP call
func fetchFromAPI(ctx context.Context, endpoint string) (string, error) {
    // Simulate variable response time
    responseTime := time.Duration(100+endpoint[0]%150) * time.Millisecond
    
    select {
    case <-time.After(responseTime):
        return fmt.Sprintf("Response from %s", endpoint), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // WithTimeout: relative duration
    ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
    defer cancel() // Always call cancel to release resources
    
    result, err := fetchFromAPI(ctx, "api.example.com/users")
    if err != nil {
        if err == context.DeadlineExceeded {
            fmt.Println("Request timed out")
        } else {
            fmt.Println("Request canceled")
        }
        return
    }
    fmt.Println(result)
    
    // WithDeadline: absolute time
    deadline := time.Now().Add(500 * time.Millisecond)
    ctx2, cancel2 := context.WithDeadline(context.Background(), deadline)
    defer cancel2()
    
    result2, _ := fetchFromAPI(ctx2, "api.example.com/orders")
    fmt.Println(result2)
}

O padrão defer cancel() garante que os recursos sejam liberados mesmo quando a operação termina antes do timeout. Omitir esta chamada causa vazamento de recursos: a goroutine do timer do context permanece ativa até que o timeout expire.

Valores Associados a Requisições com WithValue

WithValue anexa dados associados a uma requisição a um context. Casos de uso comuns incluem identificadores de requisição, tokens de autenticação e spans de rastreamento. A documentação oficial enfatiza o uso de tipos personalizados como chaves para evitar colisões.

context_values.gogo
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
)

// Define custom key types to avoid collisions
type contextKey string

const (
    requestIDKey contextKey = "requestID"
    userIDKey    contextKey = "userID"
)

// middleware adds request ID to context
func requestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = generateRequestID()
        }
        
        // Create new context with request ID
        ctx := context.WithValue(r.Context(), requestIDKey, requestID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// handler retrieves values from context
func handler(w http.ResponseWriter, r *http.Request) {
    requestID, ok := r.Context().Value(requestIDKey).(string)
    if !ok {
        requestID = "unknown"
    }
    
    log.Printf("[%s] Processing request", requestID)
    fmt.Fprintf(w, "Request ID: %s", requestID)
}

func generateRequestID() string {
    return fmt.Sprintf("req-%d", time.Now().UnixNano())
}

import "time"

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler)
    
    http.ListenAndServe(":8080", requestIDMiddleware(mux))
}

Os valores do context formam uma cadeia imutável: cada chamada a WithValue cria um novo context que envolve o pai. As buscas percorrem esta cadeia, tornando os valores profundamente aninhados mais lentos de acessar. Armazene apenas dados associados a requisições, não configuração da aplicação nem parâmetros opcionais.

Utilitários WithoutCancel e AfterFunc

O Go 1.21 adicionou WithoutCancel para operações que devem ser concluídas independentemente do cancelamento do pai, como tarefas de limpeza. AfterFunc agenda um callback quando o cancelamento do context ocorre.

utilities.gogo
package main

import (
    "context"
    "fmt"
    "log"
    "time"
)

func saveAuditLog(ctx context.Context, message string) error {
    // Use WithoutCancel: audit logs must complete even if request canceled
    cleanCtx := context.WithoutCancel(ctx)
    
    // Simulate database write
    select {
    case <-time.After(50 * time.Millisecond):
        log.Printf("Audit: %s", message)
        return nil
    case <-cleanCtx.Done():
        // This branch never executes: WithoutCancel context is never canceled
        return cleanCtx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    
    // AfterFunc: register cleanup when context ends
    stop := context.AfterFunc(ctx, func() {
        fmt.Println("Context ended, running cleanup")
    })
    
    // Perform operation
    time.Sleep(50 * time.Millisecond)
    
    // Cancel stops the AfterFunc from running if called before context ends
    if stop() {
        fmt.Println("Cleanup prevented")
    }
    
    cancel()
    
    // Audit log completes despite parent cancellation
    saveAuditLog(ctx, "Operation completed")
}

WithoutCancel preserva os valores do context pai, mas ignora seu sinal de cancelamento. Este padrão é adequado para logging, emissão de métricas e commits de banco de dados que devem ter sucesso independentemente do resultado da requisição.

Perguntas de Entrevista e Respostas

A preparação para uma entrevista técnica de Go requer compreensão dos mecanismos internos do context e melhores práticas. Estas perguntas aparecem frequentemente em entrevistas de empresas que utilizam Go para serviços backend. Para mais preparação de entrevistas Go, consulte o módulo Perguntas de entrevista sobre Context Package.

Por que o context deve ser o primeiro parâmetro?

A equipe do Go estabeleceu esta convenção para tornar a propagação do context visível e consistente. Colocar o context primeiro sinaliza aos leitores que a função respeita o cancelamento. A biblioteca padrão segue este padrão: http.Request.Context(), database/sql.QueryContext() e grpc.UnaryInterceptor esperam o context como primeiro argumento.

O que acontece se o context for armazenado em uma struct?

Armazenar o context quebra o modelo de ciclo de vida das requisições. Uma struct pode sobreviver à requisição para a qual foi criada, causando operações que usam sinais de cancelamento obsoletos ou perdem novos deadlines. A documentação do context adverte explicitamente contra este padrão.

go
// BAD: context stored in struct
type Service struct {
    ctx context.Context // Never do this
}

// GOOD: pass context to each method
type Service struct{}

func (s *Service) Process(ctx context.Context, data []byte) error {
    // Context flows through the call chain
    return s.save(ctx, data)
}

Como lidar com context em goroutines?

Goroutines devem verificar ctx.Done() em seu loop principal ou instrução select. Ignorar o channel done cria vazamentos de goroutines: a função pai retorna, mas a goroutine continua consumindo recursos.

go
// Correct pattern for long-running goroutines
func processStream(ctx context.Context, stream <-chan Data) {
    for {
        select {
        case <-ctx.Done():
            log.Println("Shutting down processor")
            return
        case data, ok := <-stream:
            if !ok {
                return
            }
            handle(data)
        }
    }
}

Quando usar context.TODO()?

Deve-se usar TODO() durante refatoração incremental ao adicionar suporte a context em código legado. Isso marca locais que precisam de propagação correta do context. O código de produção deve eventualmente substituir todas as chamadas TODO() por contexts reais provenientes de handlers de requisições ou inicialização da aplicação. Para mais informações sobre padrões de concorrência em Go, consulte Go Concurrency: Goroutines and Channels.

Erros Comuns a Evitar

O uso incorreto do context leva a bugs sutis que se manifestam sob carga ou durante sequências de encerramento. Reconhecer estes padrões previne incidentes em produção.

mistakes.gogo
package main

import (
    "context"
    "time"
)

// MISTAKE 1: Ignoring context cancellation
func badWorker(ctx context.Context) {
    for {
        // Missing select on ctx.Done()
        doExpensiveWork() // Never stops when context canceled
    }
}

// MISTAKE 2: Not calling cancel
func leakyTimeout() {
    ctx, _ := context.WithTimeout(context.Background(), time.Second)
    // Timer goroutine leaks until timeout expires
    _ = ctx
}

// MISTAKE 3: Using string keys for values
func collisionProne(ctx context.Context) context.Context {
    // Different packages might use same string key
    return context.WithValue(ctx, "userID", 123) // Bad
}

// CORRECT versions
func goodWorker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            doExpensiveWork()
        }
    }
}

func properTimeout() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel() // Always release resources
    _ = ctx
}

type userIDKey struct{}

func collisionSafe(ctx context.Context) context.Context {
    return context.WithValue(ctx, userIDKey{}, 123) // Good
}

func doExpensiveWork() {}

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Pontos-Chave para Usar Go Context em Produção

  • Passar o context como primeiro parâmetro das funções, nunca armazená-lo em structs
  • Sempre usar defer cancel() após WithTimeout, WithDeadline ou WithCancel para prevenir vazamentos de recursos
  • Usar WithCancelCause e context.Cause() para melhor depuração de erros em cadeias de cancelamento complexas
  • Verificar <-ctx.Done() em loops de goroutines para permitir encerramento gracioso e prevenir vazamentos
  • Usar tipos de chaves personalizados para WithValue para evitar colisões entre pacotes
  • Aplicar WithoutCancel para operações de limpeza que devem ser concluídas independentemente do cancelamento do pai
  • Preferir context.Background() para inicialização da aplicação e context.TODO() apenas durante refatoração
Desafio do dia

Você saberia encontrar o bug em Go?

Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador da SharpSkill

Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.

Atualizado em 29 de agosto de 2026

Tags

#go
#context
#concurrency
#interview

Compartilhar

Artigos relacionados