Go Admin

Guia Completo de Context em Go: Cancelamento, Timeout e Propagação de Valores Já leu

Context em Go: Cancelamento, Timeout e Propagação de Valores O é 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 é uma interface que encapsula um sinal de cancelamento, um deadline (prazo de expiração) e valores de dados imutáveis. A

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:

  1. 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.

  2. 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.

  3. 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.

Referências

  1. Context Package - Go Official Documentation
  2. Go Concurrency Patterns: Context
  3. Effective Go - Concurrency
  4. Using Context in Go - Dave Cheney
  5. Go in Practice - Matt Butcher & Matt Farina (Capítulo sobre Context)

Artigos relacionados