Go: Contexto do Go

Última atualização: 2026-08-26

O contexto é a pedra angular do controle de concorrência em Go — ele permite que os tempos limite, os cancelamentos e os valores sejam transmitidos de forma ordenada pela cadeia de chamadas das goroutines.

Quando seu serviço precisa lidar com dezenas de dependências externas simultaneamente (cada uma com seu próprio tempo limite e lógica de cancelamento), como você pode gerenciá-las de maneira consistente? Nesta lição, você aprenderá a usar o pacote context do Go de forma abrangente.

1. Você aprenderá


2. A história real de um engenheiro de backend

(1) Problema: Se um componente a montante atingir o tempo limite, todo o sistema entra em falha

Xiaoli é engenheira do sistema 順序. Ela é responsável por uma API REST que depende de três serviços a jusante:

“A API de ordenação chamou três serviços: o serviço de estoque, o serviço de pagamento e o serviço de notificação. Um dia, o serviço de estoque demorou 20 segundos para responder, fazendo com que o uso de memória do meu serviço disparasse, já que todas as goroutines estavam aguardando por ele — e as solicitações de outros usuários também não conseguiam ser processadas. Meu chefe perguntou: ‘Por que a página de ordenação está completamente fora do ar?’”

Análise do problema:

GO
// Bad code: no timeout control
func PlaceOrder(ctx context.Context, order Order) error {
    // If InventoryCheck hangs for 30 seconds, the goroutine waits needlessly for 30 seconds
    ok, err := InventoryCheck(ctx, order.Items)
    if err != nil {
        return err
    }
    // If the payment service times out, 30 seconds have already been wasted; the user has long since given up
    err = Charge(ctx, order.Total)
    if err != nil {
        return err
    }
    // Notification service also hangs... goroutine leak reaches the limit → OOM
    return Notify(ctx, order.UserID)
}

(2) A Solução do Go: Contexto

GO
// context_demo.go
package main

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

func main() {
    // Root Context
    root := context.Background()

    // Wrap with WithTimeout: 2-second timeout
    ctx, cancel := context.WithTimeout(root, 2*time.Second)
    defer cancel() // Ensure resources are released

    result := PlaceOrder(ctx, "Ordem-123")
    fmt.Println(result)
}

func PlaceOrder(ctx context.Context, orderID string) string {
    // Check if Context is already canceled
    select {
    case <-ctx.Done():
        return fmt.Sprintf("Canceled: %v", ctx.Err())
    default:
    }

    // Assign a shorter timeout to each downstream call
    checkCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
    defer cancel()

    ok := InventoryCheck(checkCtx, orderID)
    if !ok {
        return "Insufficient inventory"
    }
    return "Order placed successfully"
}

func InventoryCheck(ctx context.Context, orderID string) bool {
    // Simulate a slow call
    select {
    case <-time.After(500 * time.Millisecond):
        return true
    case <-ctx.Done():
        fmt.Printf("InventoryCheck canceled: %v\n", ctx.Err())
        return false
    }
}

Saída (Normal):

TEXT 📖 Somente leitura
Order placed successfully

(3) Benefícios: com contexto x sem contexto

Situação Resultado
Sem controle de tempo limite Vazamento de goroutines, OOM do sistema
Tempo manual. Após as verificações O código está disperso; cada função tem seu próprio tempo limite
Controle Unificado de Contexto Se o Contexto pai for cancelado, todos os Contextos filhos serão cancelados em cascata

3. Contexto de raiz

(1) Contexto e Tarefas pendentes

GO
package main

import (
    "context"
    "fmt"
)

func main() {
    // Background(): root Context, never canceled
    // Used in main functions, initialization, top-level requests
    ctx := context.Background()
    fmt.Printf("Background: %v\n", ctx)

    // TODO(): placeholder when unsure which Context to use
    // Marks code that has not yet been integrated with Context and needs refactoring
    todo := context.TODO()
    fmt.Printf("TODO: %v\n", todo)
}

(2) Contexto vs. Tarefas pendentes

Contexto Objetivo Será cancelado?
Background() Nó raiz, ponto de partida para todos os contextos Nunca
TODO() Espaço reservado indicando que o código ainda não foi integrado ao Context Nunca
💡 Dica: context.Background() é o nó raiz de todas as árvores de contexto e nunca é cancelado. context.TODO() é usado para marcar código que ainda não foi integrado à cadeia de contexto — você deve substituí-lo por um contexto apropriado o mais rápido possível.


4. context.WithCancel: Cancelamento manual

(1) ▶ Exemplo: Cancelamento manual de uma goroutine

GO
package main

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

func Worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopped: %v\n", id, ctx.Err())
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    go Worker(ctx, 1)
    go Worker(ctx, 2)

    time.Sleep(2 * time.Second)
    fmt.Println("Main goroutine initiating cancellation...")
    cancel() // Tell all Workers to stop

    // Wait for goroutines to exit
    time.Sleep(500 * time.Millisecond)
}
▶ Experimente

(2) ▶ Exemplo: Cancelamento em cascata

GO
package main

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

func handler(ctx context.Context) {
    // Child Context inherits parent Context
    childCtx, cancel := context.WithCancel(ctx)
    defer cancel()

    go subTask(childCtx, "task-1")
    go subTask(childCtx, "task-2")

    // Parent Context canceled → Child Context automatically canceled
    select {
    case <-time.After(1 * time.Second):
        fmt.Println("Handler complete")
    case <-ctx.Done():
        fmt.Println("Handler canceled")
    }
}

func subTask(ctx context.Context, name string) {
    select {
    case <-time.After(3 * time.Second):
        fmt.Printf("%s complete\n", name)
    case <-ctx.Done():
        fmt.Printf("%s canceled: %v\n", name, ctx.Err())
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go handler(ctx)

    time.Sleep(500 * time.Millisecond)
    cancel() // Cancel → handler → subTask all cascadingly canceled

    time.Sleep(1 * time.Second)
}
▶ Experimente
100%
sequenceDiagram
    participant Main as main()
    participant H as handler
    participant ST1 as subTask-1
    participant ST2 as subTask-2

    Main->>H: WithCancel
    H->>ST1: WithCancel
    H->>ST2: WithCancel
    Note over Main,ST2: Normal execution
    Main->>Main: cancel()
    Main-->>H: ctx.Done()
    H-->>ST1: ctx.Done()
    H-->>ST2: ctx.Done()
    Note over Main,ST2: All cascadingly canceled
🔥 Erro comum: É necessário chamar cancel(). Mesmo que você use WithTimeout, você deve adiar o cancel(). Caso contrário, os recursos do Context (temporizadores, goroutines) não serão liberados. Regra: Ao criar um WithCancel, WithTimeout ou WithDeadline → execute imediatamente defer cancel().


5. context.WithTimeout: Cancelamento automático ao atingir o tempo limite

(1) ▶ Exemplo: Controle de tempo limite

GO
package main

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

func callExternalAPI(ctx context.Context, name string, delay time.Duration) (string, error) {
    select {
    case <-time.After(delay):
        return fmt.Sprintf("%s response", name), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // 1-second timeout
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    // Call two downstream services
    result1 := make(chan string, 1)
    result2 := make(chan string, 1)

    go func() {
        r, err := callExternalAPI(ctx, "ServiceA", 500*time.Millisecond)
        if err != nil {
            result1 <- fmt.Sprintf("ServiceA failed: %v", err)
            return
        }
        result1 <- r
    }()

    go func() {
        r, err := callExternalAPI(ctx, "ServiceB", 1500*time.Millisecond)
        if err != nil {
            result2 <- fmt.Sprintf("ServiceB failed: %v", err)
            return
        }
        result2 <- r
    }()

    fmt.Println(<-result1) // ServiceA response (500ms < 1s timeout)
    fmt.Println(<-result2) // ServiceB failed: context deadline exceeded (1500ms > 1s)
}
▶ Experimente

(2) ▶ Exemplo: WithTimeout x WithDeadline

GO
package main

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

func main() {
    // WithTimeout: timeout 2 seconds from now
    timeoutCtx, cancel1 := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel1()

    // WithDeadline: specify absolute time
    deadline := time.Now().Add(2 * time.Second)
    deadlineCtx, cancel2 := context.WithDeadline(context.Background(), deadline)
    defer cancel2()

    // Both have the same effect
    fmt.Printf("timeoutCtx deadline: %v\n", timeoutCtx.Deadline())
    fmt.Printf("deadlineCtx deadline: %v\n", deadlineCtx.Deadline())
}
▶ Experimente

(3) WithTimeout x WithDeadline

Método Parâmetros Finalidade
WithTimeout(parent, 2*time.Second) Tempo relativo Mais comum: “Espere até 2 segundos”
WithDeadline(parent, time.Time) Tempo absoluto Especifica um prazo: “Concluir até às 15h30”

6. Passagem de valores no nível da solicitação usando context.WithValue

(1) ▶ Exemplo: WithValue

GO
package main

import (
    "context"
    "fmt"
)

// Custom key type (avoids conflicts)
type contextKey string

const (
    UserIDKey    contextKey = "user_id"
    TraceIDKey   contextKey = "trace_id"
    RequestIDKey contextKey = "request_id"
)

func middleware(ctx context.Context) context.Context {
    // Get trace ID from request header
    ctx = context.WithValue(ctx, TraceIDKey, "trace-123")
    ctx = context.WithValue(ctx, RequestIDKey, "req-456")
    return ctx
}

func handler(ctx context.Context, userID string) {
    ctx = context.WithValue(ctx, UserIDKey, userID)

    // Pass to business layer
    service(ctx)
}

func service(ctx context.Context) {
    // Retrieve values from Context
    userID := ctx.Value(UserIDKey).(string)
    traceID := ctx.Value(TraceIDKey).(string)
    requestID := ctx.Value(RequestIDKey).(string)

    fmt.Printf("Processing request: user=%s, trace=%s, request=%s\n",
        userID, traceID, requestID)
}

func main() {
    ctx := context.Background()
    ctx = middleware(ctx)
    handler(ctx, "user-007")
}
▶ Experimente
🔥 Erro comum: O key em context.WithValue deve ser um tipo personalizado; não é possível usar uma string diretamente. Se dois pacotes usarem a string "user_id" como chave, ocorrerá um conflito. Usar o tipo personalizado type contextKey string é a prática recomendada.

(2) Casos de uso do WithValue

Cenário Recomendado Não recomendado
TraceID / RequestID ✅ Passado via WithValue ❌ Variável global
Token de autenticação ✅ Passado por WithValue ❌ Parâmetro de função
Conexão com o banco de dados ❌ Obtida por meio de injeção de dependência ❌ WithValue
Parâmetro de negócio ❌ Parâmetro explícito ❌ Passagem implícita com valor

7. Regras de encadeamento de contexto

GO
package main

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

type contextKey string

func main() {
    root := context.Background()

    // Chaining: WithCancel → WithTimeout → WithValue
    ctx1, cancel1 := context.WithCancel(root)
    defer cancel1()

    ctx2, cancel2 := context.WithTimeout(ctx1, 2*time.Second)
    defer cancel2()

    ctx3 := context.WithValue(ctx2, contextKey("trace"), "trace-007")

    // ctx3 inherits ctx1's cancellation + ctx2's timeout + ctx3's value
    fmt.Printf("ctx3 deadline: %v\n", ctx3.Deadline())
    fmt.Printf("ctx3 value: %v\n", ctx3.Value(contextKey("trace")))

    // Cancel ctx1 first → ctx2 and ctx3 both receive the cancellation signal
    cancel1()
    time.Sleep(10 * time.Millisecond)
    fmt.Printf("ctx2 err: %v\n", ctx2.Err())
    fmt.Printf("ctx3 err: %v\n", ctx3.Err())
}

(1) Regras para a passagem de contexto

Regra Descrição
Primeiro parâmetro O primeiro parâmetro na assinatura de uma função é sempre ctx context.Context
Não armazene em uma estrutura Não armazene Context em um campo de estrutura; em vez disso, passe-o como parâmetro
Passagem entre funções Cada função que precisa estar ciente de cancelamentos ou tempos limite recebe um Contexto
Cadeia imutável Cada chamada ao WithCancel/WithTimeout/WithValue retorna um novo Context
Cancelamento em cascata Ao cancelar um item pai → todos os itens filhos são cancelados; o cancelamento de um item filho não afeta o item pai

8. Exemplo completo: Controle de tempo limite para rastreamento de microsserviços

GO
// microservice_chain.go
package main

import (
    "context"
    "fmt"
    "math/rand"
    "time"
)

// ---------- Simulated downstream services ----------

// Inventory service
type InventoryService struct{}

func (s *InventoryService) Check(ctx context.Context, orderID string) (bool, error) {
    // Simulate random delay
    delay := time.Duration(rand.Intn(1500)) * time.Millisecond
    select {
    case <-time.After(delay):
        return true, nil
    case <-ctx.Done():
        return false, ctx.Err()
    }
}

// Payment service
type PaymentService struct{}

func (s *PaymentService) Charge(ctx context.Context, amount float64) (string, error) {
    delay := time.Duration(rand.Intn(1500)) * time.Millisecond
    select {
    case <-time.After(delay):
        return "pay-" + fmt.Sprintf("%d", time.Now().UnixNano()), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

// Notification service
type NotificationService struct{}

func (s *NotificationService) Send(ctx context.Context, userID, message string) error {
    delay := time.Duration(rand.Intn(1500)) * time.Millisecond
    select {
    case <-time.After(delay):
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

// ---------- Business layer ----------

type OrderService struct {
    inventory *InventoryService
    payment   *PaymentService
    notify    *NotificationService
}

func NewOrderService() *OrderService {
    return &OrderService{
        inventory: &InventoryService{},
        payment:   &PaymentService{},
        notify:    &NotificationService{},
    }
}

// PlaceOrder uses Context to control the entire chain's timeout
func (s *OrderService) PlaceOrder(ctx context.Context, userID, orderID string, amount float64) error {
    // 1. Inventory check (wait at most 1 second)
    invCtx, invCancel := context.WithTimeout(ctx, 1*time.Second)
    defer invCancel()

    ok, err := s.inventory.Check(invCtx, orderID)
    if err != nil {
        return fmt.Errorf("inventory check failed: %w", err)
    }
    if !ok {
        return fmt.Errorf("insufficient inventory")
    }

    // 2. Payment (wait at most 2 seconds)
    payCtx, payCancel := context.WithTimeout(ctx, 2*time.Second)
    defer payCancel()

    paymentID, err := s.payment.Charge(payCtx, amount)
    if err != nil {
        return fmt.Errorf("payment failed: %w", err)
    }

    // 3. Notification (wait at most 500ms)
    notifyCtx, notifyCancel := context.WithTimeout(ctx, 500*time.Millisecond)
    defer notifyCancel()

    err = s.notify.Send(notifyCtx, userID, "Order placed: "+orderID)
    if err != nil {
        // Notification failure does not affect the order (async log recording)
        fmt.Printf("Notification failed (logged): %v\n", err)
    }

    fmt.Printf("Order placed successfully: user=%s, order=%s, payment=%s\n", userID, orderID, paymentID)
    return nil
}

// ---------- Client ----------

func main() {
    svc := NewOrderService()

    // Overall request timeout of 3 seconds
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    err := svc.PlaceOrder(ctx, "user-007", "order-123", 99.99)
    if err != nil {
        fmt.Printf("Order failed: %v\n", err)
    }
}
💡 Dica: O método Err() de Context retorna um dos dois valores possíveis: context.Canceled (cancelado manualmente) ou context.DeadlineExceeded (excedido o tempo limite). Dentro de uma cadeia, você pode usar errors.Is(err, context.DeadlineExceeded) para determinar que tipo de cancelamento ocorreu e definir uma estratégia de repetição de acordo com isso.


❓ Perguntas Frequentes

P: Quando se deve usar o Context? R: Em qualquer cenário que exija controle de tempo limite, cancelamento manual ou passagem de valores no nível da solicitação. Isso inclui: processamento de solicitações HTTP, consultas a bancos de dados, chamadas RPC e tarefas agendadas. Não use o Context em funções puramente computacionais que não exijam cancelamento ou controle de tempo limite.

P: Qual é a diferença entre Background() e TODO()? R: Ambos são contextos raiz que nunca podem ser cancelados, mas têm significados diferentes: Background() é o contexto raiz que você está usando ativamente em seu código; TODO() é um marcador de lugar, indicando que essa seção do código ainda não foi integrada ao Contexto e precisa ser refatorada o mais rápido possível.

P: O que devo escolher, WithTimeout ou WithDeadline? R: Na maioria dos casos, use WithTimeout (“aguarde até 2 segundos” é mais intuitivo). Use WithDeadline apenas quando o tempo limite for um tempo absoluto (“concluir até às 15:30:00”). WithTimeout chama internamente WithDeadline.

P: O Context pode ser cancelado várias vezes? R: O cancelamento só pode ser acionado uma vez — chamadas repetidas ao cancel() são seguras (a segunda chamada e as seguintes não produzem efeito). Todas as goroutines que estiverem aguardando o Done() receberão o sinal apenas uma vez.

P: O que acontece quando um cancelamento do contexto pai e um tempo limite ocorrem simultaneamente? R: Os dois são acionados de forma independente, e o que ocorrer primeiro entra em vigor. Se o contexto pai for cancelado manualmente, todos os contextos filhos recebem imediatamente um sinal Done(), mesmo que os tempos limite dos contextos filhos ainda não tenham expirado. Isso garante o caminho mais curto para cancelamentos em cascata.

P: O valor passado para WithValue pode ser modificado? R: Não. O valor de context.WithValue é imutável. O que se entende por “modificação” envolve, essencialmente, a criação de um novo Contexto. O WithValue de um Contexto filho não afeta o Contexto pai. Isso garante a segurança de concorrência.

P: Para quais tipos de dados o Context é adequado? R: Ele é adequado apenas para armazenar metadados no nível da solicitação: TraceID, RequestID, UserID e tokens de autenticação. Não é adequado para armazenar parâmetros de negócios (como preço ou quantidade), nem para armazenar conexões ou configurações de banco de dados — esses devem ser passados por meio de injeção de dependência.


📖 Resumo


📝 Exercícios

  1. Exercício básico (Dificuldade ⭐): Escreva uma função FetchWithTimeout(ctx, url string, timeout time.Duration) que utilize context.WithTimeout para controlar o tempo limite das solicitações HTTP. Quando o tempo limite expirar, a solicitação HTTP subjacente deve ser automaticamente cancelada.

  2. Problema avançado (Dificuldade ⭐⭐): Implemente um gerenciador de tarefas agendadas que possam ser canceladas simultaneamente. Ele deve suportar: (1) o registro de múltiplas tarefas agendadas (uma por goroutine); (2) o cancelamento individual (chamando a função cancel); (3) o cancelamento em lote (encadeamento de WithCancel); (4) Todas as tarefas compartilham um único contexto raiz.

  3. Desafio (Dificuldade ⭐⭐⭐): Simule um sistema de rastreamento distribuído. Requisitos: (1) Passe o TraceID usando WithValue por três camadas de chamadas de função (API → Serviço → Banco de Dados); (2) Cada camada possui seu próprio controle de tempo limite (Serviço: 2 s, Banco de Dados: 500 ms); (3) Se ocorrer um tempo limite, a camada atual é cancelada, mas isso não afeta as camadas a montante; (4) Exiba o tempo decorrido e o TraceID para cada etapa. Use Deadline() a partir de Context para calcular o tempo restante.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%