O que é Context em Go?

O context é um mecanismo da biblioteca padrão de Go para transportar deadlines, sinais de cancelamento e valores request-scoped entre fronteiras de API e goroutines. Definido no pacote context, ele resolve um problema fundamental em sistemas concorrentes: como comunicar que uma operação deve ser interrompida quando o resultado não é mais necessário.

Imagine uma requisição HTTP que dispara consultas ao banco de dados, chamadas a APIs externas e processamento em background. Se o cliente desconecta no meio do caminho, todas essas operações pendentes deveriam parar imediatamente — caso contrário, você desperdiça recursos do servidor. O context é o mecanismo que propaga esse sinal de “pare de trabalhar” por toda a cadeia de chamadas.

O pacote context foi introduzido na biblioteca padrão no Go 1.7, após anos como pacote experimental em golang.org/x/net/context. Hoje, é tão fundamental que praticamente toda interface da biblioteca padrão que faz I/O aceita um context.Context como primeiro parâmetro — de HTTP handlers a operações de banco de dados e chamadas gRPC.

Contextos raiz: Background e TODO

Todo context deriva de um dos dois contextos raiz:

// Contexto vazio — usado como raiz em main(), init() e testes
ctx := context.Background()

// Contexto placeholder — quando não se sabe qual contexto usar ainda
ctx := context.TODO()

Background() é o ponto de partida mais comum. Use-o em main(), na inicialização de serviços e como contexto raiz de requisições quando nenhum outro contexto está disponível.

TODO() existe para marcar no código que você ainda precisa decidir qual contexto usar. Ferramentas de análise estática podem detectar usos de TODO() em produção como potenciais problemas.

WithCancel — cancelamento manual

WithCancel cria um contexto filho que pode ser cancelado explicitamente:

func processarDados(ctx context.Context) {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel() // SEMPRE chame cancel para liberar recursos

    go func() {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("Cancelado:", ctx.Err())
                return
            default:
                // processamento contínuo
                fmt.Println("Processando...")
                time.Sleep(500 * time.Millisecond)
            }
        }
    }()

    // Simular cancelamento após 2 segundos
    time.Sleep(2 * time.Second)
    cancel() // sinaliza cancelamento
    time.Sleep(100 * time.Millisecond) // tempo para goroutine finalizar
}

A regra de ouro: sempre chame cancel(), geralmente com defer. Não chamar cancel() causa vazamento de recursos internos do context, mesmo quando o contexto pai é cancelado.

Padrão com AfterFunc (Go 1.21+)

ctx, cancel := context.WithCancel(ctx)
defer cancel()

stop := context.AfterFunc(ctx, func() {
    fmt.Println("Executando cleanup após cancelamento")
})
defer stop()

WithTimeout — cancelamento por tempo

WithTimeout é o padrão mais usado em aplicações web — define um tempo máximo para uma operação:

func buscarUsuario(ctx context.Context, id int) (*Usuario, error) {
    // Timeout de 3 segundos para a operação completa
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, "GET",
        fmt.Sprintf("https://api.exemplo.com/users/%d", id), nil)
    if err != nil {
        return nil, err
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        if ctx.Err() == context.DeadlineExceeded {
            return nil, fmt.Errorf("timeout ao buscar usuário %d", id)
        }
        return nil, err
    }
    defer resp.Body.Close()

    var usuario Usuario
    if err := json.NewDecoder(resp.Body).Decode(&usuario); err != nil {
        return nil, err
    }
    return &usuario, nil
}

Na prática, WithTimeout(ctx, 3*time.Second) é equivalente a WithDeadline(ctx, time.Now().Add(3*time.Second)). Use WithTimeout para durações relativas e WithDeadline para momentos absolutos.

WithDeadline — cancelamento por horário

WithDeadline define um momento exato no tempo como limite:

func processarRelatorio(ctx context.Context) error {
    // Deadline: meia-noite de hoje
    meianoite := time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour)
    ctx, cancel := context.WithDeadline(ctx, meianoite)
    defer cancel()

    // Verificar quanto tempo resta
    if deadline, ok := ctx.Deadline(); ok {
        restante := time.Until(deadline)
        fmt.Printf("Tempo restante: %v\n", restante)
    }

    // Operações de longa duração...
    return nil
}

Herança de deadlines

Uma propriedade crucial: o deadline efetivo é sempre o menor entre o pai e o filho:

// Pai com timeout de 10 segundos
ctxPai, cancelPai := context.WithTimeout(context.Background(), 10*time.Second)
defer cancelPai()

// Filho com timeout de 5 segundos — efetivo: 5s
ctxFilho5, cancel5 := context.WithTimeout(ctxPai, 5*time.Second)
defer cancel5()

// Filho com timeout de 30 segundos — efetivo: 10s (herda do pai)
ctxFilho30, cancel30 := context.WithTimeout(ctxPai, 30*time.Second)
defer cancel30()

WithValue — transportando valores

WithValue anexa pares chave-valor ao contexto para dados request-scoped:

// Defina tipos custom para chaves (evita colisões)
type chaveContexto string

const (
    chaveRequestID chaveContexto = "request_id"
    chaveUsuario   chaveContexto = "usuario"
)

func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := uuid.New().String()
        ctx := context.WithValue(r.Context(), chaveRequestID, requestID)
        ctx = context.WithValue(ctx, chaveUsuario, "diego")

        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func handler(w http.ResponseWriter, r *http.Request) {
    requestID, ok := r.Context().Value(chaveRequestID).(string)
    if !ok {
        requestID = "desconhecido"
    }
    fmt.Fprintf(w, "Request ID: %s", requestID)
}

Regras para WithValue

  1. Use tipos custom para chaves — nunca use string ou int diretamente para evitar colisões entre packages
  2. Apenas para dados request-scoped — request ID, token de autenticação, trace ID
  3. Nunca para parâmetros de função — se uma função precisa de um valor, passe como parâmetro explícito
  4. Valores devem ser thread-safe — o context é compartilhado entre goroutines

Context em HTTP handlers

O pacote net/http integra context nativamente:

func meuHandler(w http.ResponseWriter, r *http.Request) {
    // O request já carrega um context
    ctx := r.Context()

    // Adicionar timeout para processamento
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    // Passar o context para operações downstream
    resultado, err := consultarBanco(ctx)
    if err != nil {
        if ctx.Err() == context.DeadlineExceeded {
            http.Error(w, "Timeout", http.StatusGatewayTimeout)
            return
        }
        http.Error(w, "Erro interno", http.StatusInternalServerError)
        return
    }

    json.NewEncoder(w).Encode(resultado)
}

Quando o cliente desconecta, o context do request é cancelado automaticamente. Todas as operações que usam esse context recebem o sinal e podem parar de forma limpa. Isso é essencial para APIs REST e microsserviços em produção.

Context com banco de dados

Toda operação de banco de dados em Go deve receber um context:

func buscarProdutos(ctx context.Context, db *sql.DB, categoria string) ([]Produto, error) {
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

    rows, err := db.QueryContext(ctx,
        "SELECT id, nome, preco FROM produtos WHERE categoria = $1",
        categoria)
    if err != nil {
        return nil, fmt.Errorf("query falhou: %w", err)
    }
    defer rows.Close()

    var produtos []Produto
    for rows.Next() {
        var p Produto
        if err := rows.Scan(&p.ID, &p.Nome, &p.Preco); err != nil {
            return nil, err
        }
        produtos = append(produtos, p)
    }
    return produtos, rows.Err()
}

Se o context for cancelado durante a query, o driver do banco de dados interrompe a operação. Isso previne queries lentas de monopolizarem conexões do pool — algo crítico para aplicações com PostgreSQL.

Propagação de context

O padrão correto é propagar o context por toda a cadeia de chamadas:

func ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    usuario, err := autenticar(ctx)        // propaga
    pedidos, err := buscarPedidos(ctx, usuario.ID) // propaga
    total := calcularTotal(pedidos)         // não precisa (cálculo puro)
    err = enviarEmail(ctx, usuario.Email, total)   // propaga
}

Regra prática: propague context para qualquer func que faça I/O (rede, disco, banco) ou coordene goroutines. Funções de cálculo puro sem I/O geralmente não precisam de context.

Boas práticas com context

  1. Context é sempre o primeiro parâmetrofunc Foo(ctx context.Context, ...)
  2. Nunca armazene context em structs — passe-o como parâmetro
  3. Sempre chame cancel() — use defer logo após a criação
  4. Não use WithValue como saco de dados — apenas para dados request-scoped
  5. Verifique ctx.Err() antes de operações custosas — evita trabalho desnecessário
  6. Prefira context.Background() sobre context.TODO() em código de produção

Para aprofundar em padrões de concorrência com context, explore os padrões de concorrência em Go e entenda como context trabalha com channels e select.

Perguntas frequentes (FAQ)

Qual a diferença entre context.Background() e context.TODO()?

Funcionalmente são idênticos — ambos criam um context vazio que nunca é cancelado. A diferença é semântica: Background() é usado quando você sabe que precisa de um context raiz (em main(), testes, inicialização). TODO() indica que você ainda não decidiu qual context usar — serve como marcador para revisão futura.

Posso armazenar context dentro de um struct?

Não — essa é uma das poucas regras absolutas do Go. O context deve ser passado como primeiro parâmetro de funções, nunca armazenado em campos de structs. Armazenar contextos em structs dificulta o rastreamento de cancelamento e cria riscos de uso de contextos expirados.

Como fazer timeout em chamadas HTTP com context?

Use http.NewRequestWithContext(ctx, method, url, body) para criar um request que respeita o context. Combine com context.WithTimeout para definir o tempo máximo. Se o timeout expirar, o client cancela a requisição e retorna um erro com context.DeadlineExceeded.

Context.WithValue é thread-safe?

Sim. O context é imutável — cada WithValue cria um novo context filho sem modificar o pai. Múltiplas goroutines podem ler valores do mesmo context simultaneamente sem risco de race condition. Porém, os valores armazenados devem ser thread-safe por si mesmos.