Golang

O pacote context: propagação de cancelamento e prazos Já leu

10 min de leitura

O pacote context: propagação de cancelamento e prazos
Imagine um servidor HTTP recebendo uma requisição que desencadeia três operações: uma consulta ao banco de dados, uma chamada a uma API externa e uma busca em cache. Cada operação acontece em sua própria goroutine. No me

Imagine um servidor HTTP recebendo uma requisição que desencadeia três operações: uma consulta ao banco de dados, uma chamada a uma API externa e uma busca em cache. Cada operação acontece em sua própria goroutine. No meio do caminho, o cliente cancela a requisição — fecha a conexão ou atinge o timeout do lado dele.

Sem um mecanismo de propagação, as três operações continuariam executando, consumindo recursos, fazendo queries e chamadas de rede para produzir um resultado que nunca será usado. Em um servidor sob carga, esse desperdício acumula e degrada toda a aplicação.

O pacote context resolve exatamente esse problema. Ele fornece um objeto que carrega um sinal de cancelamento, um prazo e valores arbitrários, e pode ser propagado por toda a árvore de chamadas de uma requisição — através de fronteiras de goroutines, chamadas HTTP, consultas a banco de dados e qualquer outra operação que aceite um contexto.


A interface Context

O contexto é definido por uma interface simples de quatro métodos:

type Context interface {
    // Retorna o prazo final, se houver
    Deadline() (deadline time.Time, ok bool)

    // Channel fechado quando o contexto é cancelado
    Done() <-chan struct{}

    // Motivo do cancelamento — nil se ainda não cancelado
    Err() error

    // Valor associado a uma chave — para dados de requisição
    Value(key any) any
}

Quatro implementações concretas formam a base do sistema: context.Background(), context.TODO(), e os contextos derivados criados por WithCancel, WithDeadline, WithTimeout e WithValue.


Os contextos raiz

Todo contexto começa a partir de um dos dois contextos raiz:

// Background — contexto raiz para a aplicação inteira
// Nunca é cancelado, não tem deadline, não tem valores
ctx := context.Background()

// TODO — placeholder para código onde o contexto ainda não foi definido
// Semanticamente indica "preciso adicionar context aqui depois"
ctx := context.TODO()

context.Background() é o ponto de partida em main, em testes e em goroutines de nível superior. context.TODO() é uma marcação intencional para código em transição — o analisador estático golangci-lint pode identificá-los e lembrar o desenvolvedor de substituí-los.


WithCancel: cancelamento manual

package main

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

func monitorarSistema(ctx context.Context, nome string) {
    ticker := time.NewTicker(200 * time.Millisecond)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            fmt.Printf("[%s] sistema operacional\n", nome)
        case <-ctx.Done():
            fmt.Printf("[%s] monitoramento encerrado: %v\n", nome, ctx.Err())
            return
        }
    }
}

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

    go monitorarSistema(ctx, "CPU")
    go monitorarSistema(ctx, "Memória")
    go monitorarSistema(ctx, "Disco")

    time.Sleep(600 * time.Millisecond)

    fmt.Println("cancelando todos os monitores...")
    cancelar() // propaga para todas as goroutines simultaneamente

    time.Sleep(100 * time.Millisecond)
    fmt.Println("encerrado")
}

cancelar() fecha o channel Done() do contexto e de todos os seus filhos simultaneamente. Todas as goroutines que aguardam <-ctx.Done() desbloqueiam ao mesmo tempo — um broadcast eficiente sem nenhuma comunicação adicional.


WithTimeout e WithDeadline

WithTimeout recebe uma duração relativa ao momento atual. WithDeadline recebe um instante absoluto no tempo. Ambos cancelam automaticamente o contexto quando o prazo é atingido:

package main

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

func buscarDados(ctx context.Context, fonte string, latencia time.Duration) (string, error) {
    select {
    case <-time.After(latencia):
        return fmt.Sprintf("dados de %s", fonte), nil
    case <-ctx.Done():
        return "", fmt.Errorf("%s: %w", fonte, ctx.Err())
    }
}

func main() {
    // WithTimeout — deadline relativo
    ctx, cancelar := context.WithTimeout(context.Background(), 300*time.Millisecond)
    defer cancelar()

    dados, err := buscarDados(ctx, "banco_primario", 200*time.Millisecond)
    if err != nil {
        fmt.Println("Erro:", err)
    } else {
        fmt.Println("Sucesso:", dados)
    }

    // WithDeadline — deadline absoluto
    prazo := time.Now().Add(100 * time.Millisecond)
    ctxDL, cancelDL := context.WithDeadline(context.Background(), prazo)
    defer cancelDL()

    dados, err = buscarDados(ctxDL, "banco_secundario", 300*time.Millisecond)
    if err != nil {
        fmt.Println("Erro:", err) // context deadline exceeded
    } else {
        fmt.Println("Sucesso:", dados)
    }

    // Verificando o deadline
    if dl, ok := ctxDL.Deadline(); ok {
        fmt.Printf("Deadline foi: %s\n", dl.Format("15:04:05.000"))
    }
}

A árvore de contextos

Contextos formam uma árvore hierárquica. Cancelar um contexto pai cancela todos os filhos — mas cancelar um filho não afeta o pai nem os irmãos:

package main

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

func executar(ctx context.Context, nome string, duracao time.Duration) {
    select {
    case <-time.After(duracao):
        fmt.Printf("[%s] concluído normalmente\n", nome)
    case <-ctx.Done():
        fmt.Printf("[%s] cancelado: %v\n", nome, ctx.Err())
    }
}

func main() {
    // Raiz
    raiz, cancelarRaiz := context.WithCancel(context.Background())
    defer cancelarRaiz()

    // Filho A — timeout de 400ms
    filhoA, cancelarA := context.WithTimeout(raiz, 400*time.Millisecond)
    defer cancelarA()

    // Filho B — timeout de 800ms
    filhoB, cancelarB := context.WithTimeout(raiz, 800*time.Millisecond)
    defer cancelarB()

    // Neto de A — timeout de 200ms
    neto, cancelarNeto := context.WithTimeout(filhoA, 200*time.Millisecond)
    defer cancelarNeto()

    go executar(filhoA, "filho-A", 600*time.Millisecond)  // será cancelado por filhoA
    go executar(filhoB, "filho-B", 600*time.Millisecond)  // concluirá normalmente
    go executar(neto, "neto", 600*time.Millisecond)       // será cancelado por neto (200ms)

    time.Sleep(1 * time.Second)
    // neto cancela em 200ms
    // filhoA cancela em 400ms
    // filhoB conclui normalmente em 600ms (antes dos 800ms)
}

WithValue: transportando dados de requisição

context.WithValue associa um par chave-valor ao contexto. Valores propagam pela árvore — filhos herdam os valores dos pais:

package main

import (
    "context"
    "fmt"
)

// Tipos próprios para chaves — evita colisões com outros pacotes
type chaveContexto string

const (
    ChaveRequisicaoID chaveContexto = "requisicao_id"
    ChaveUsuarioID    chaveContexto = "usuario_id"
    ChavePapel        chaveContexto = "papel"
)

func middleware(ctx context.Context, reqID, userID, papel string) context.Context {
    ctx = context.WithValue(ctx, ChaveRequisicaoID, reqID)
    ctx = context.WithValue(ctx, ChaveUsuarioID, userID)
    ctx = context.WithValue(ctx, ChavePapel, papel)
    return ctx
}

func obterRequisicaoID(ctx context.Context) string {
    v, _ := ctx.Value(ChaveRequisicaoID).(string)
    return v
}

func obterUsuarioID(ctx context.Context) string {
    v, _ := ctx.Value(ChaveUsuarioID).(string)
    return v
}

func processarPedido(ctx context.Context, pedidoID int) {
    reqID := obterRequisicaoID(ctx)
    userID := obterUsuarioID(ctx)
    papel, _ := ctx.Value(ChavePapel).(string)

    fmt.Printf("[req:%s] usuário %s (papel:%s) processando pedido %d\n",
        reqID, userID, papel, pedidoID)
}

func main() {
    ctx := context.Background()
    ctx = middleware(ctx, "req-abc-123", "user-42", "admin")

    processarPedido(ctx, 1001)
    processarPedido(ctx, 1002)
}

Regras importantes para WithValue:

Use tipos próprios como chave — nunca strings ou ints diretamente — para evitar colisões entre pacotes diferentes. Use WithValue apenas para dados de escopo de requisição: IDs de rastreamento, tokens de autenticação, metadados de telemetria. Nunca use para passar parâmetros opcionais de funções — isso torna as dependências implícitas e dificulta testes.


context em handlers HTTP

O caso de uso mais comum em produção. O net/http injeta automaticamente um contexto em cada requisição, que é cancelado quando a conexão do cliente se encerra:

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "time"
)

type chaveHTTP string

const ChaveRequestID chaveHTTP = "request_id"

// Middleware que injeta request ID e timeout
func comContexto(prox http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Timeout de 5 segundos por requisição
        ctx, cancelar := context.WithTimeout(r.Context(), 5*time.Second)
        defer cancelar()

        // Injeta request ID
        reqID := r.Header.Get("X-Request-ID")
        if reqID == "" {
            reqID = fmt.Sprintf("req-%d", time.Now().UnixNano())
        }
        ctx = context.WithValue(ctx, ChaveRequestID, reqID)

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

func consultarBancoDados(ctx context.Context, query string) (map[string]any, error) {
    // Simula consulta que respeita o contexto
    select {
    case <-time.After(200 * time.Millisecond):
        return map[string]any{"query": query, "resultado": "dados"}, nil
    case <-ctx.Done():
        return nil, fmt.Errorf("consulta cancelada: %w", ctx.Err())
    }
}

func handlerUsuario(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    reqID, _ := ctx.Value(ChaveRequestID).(string)

    dados, err := consultarBancoDados(ctx, "SELECT * FROM usuarios")
    if err != nil {
        log.Printf("[%s] erro: %v", reqID, err)
        http.Error(w, "erro interno", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("X-Request-ID", reqID)
    json.NewEncoder(w).Encode(dados)
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/usuarios", handlerUsuario)

    handler := comContexto(mux)

    servidor := &http.Server{
        Addr:    ":8080",
        Handler: handler,
    }

    log.Println("servidor em :8080")
    log.Fatal(servidor.ListenAndServe())
}

context com banco de dados

A biblioteca database/sql aceita context em todos os métodos de consulta, permitindo cancelar queries em andamento:

package main

import (
    "context"
    "database/sql"
    "fmt"
    "time"

    _ "github.com/lib/pq"
)

type RepositorioUsuario struct {
    db *sql.DB
}

func (r *RepositorioUsuario) BuscarPorID(ctx context.Context, id int) error {
    // Context propaga para o driver — cancela a query no banco se necessário
    row := r.db.QueryRowContext(ctx,
        "SELECT id, nome, email FROM usuarios WHERE id = $1", id)

    var (
        userID int
        nome   string
        email  string
    )

    if err := row.Scan(&userID, &nome, &email); err != nil {
        return fmt.Errorf("BuscarPorID: %w", err)
    }

    fmt.Printf("usuário: %d %s <%s>\n", userID, nome, email)
    return nil
}

func (r *RepositorioUsuario) ListarAtivos(ctx context.Context) error {
    rows, err := r.db.QueryContext(ctx,
        "SELECT id, nome FROM usuarios WHERE ativo = true ORDER BY nome")
    if err != nil {
        return fmt.Errorf("ListarAtivos: %w", err)
    }
    defer rows.Close()

    for rows.Next() {
        // Verifica cancelamento entre linhas
        select {
        case <-ctx.Done():
            return fmt.Errorf("ListarAtivos cancelado: %w", ctx.Err())
        default:
        }

        var id int
        var nome string
        if err := rows.Scan(&id, &nome); err != nil {
            return err
        }
        fmt.Printf("%d: %s\n", id, nome)
    }

    return rows.Err()
}

func exemplo() {
    // Exemplo de uso com timeout
    ctx, cancelar := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancelar()

    repo := &RepositorioUsuario{ /* db inicializado */ }
    if err := repo.BuscarPorID(ctx, 42); err != nil {
        fmt.Println("erro:", err)
    }
}

Boas práticas consolidadas

Context deve ser o primeiro parâmetro, sempre nomeado ctx:

// Correto
func ProcessarPedido(ctx context.Context, pedidoID int) error

// Errado
func ProcessarPedido(pedidoID int, ctx context.Context) error
func ProcessarPedido(c context.Context, pedidoID int) error

Nunca armazene context em structs. Context deve fluir pela pilha de chamadas como parâmetro de função, não ser armazenado e reutilizado:

// Errado
type Servico struct {
    ctx context.Context
}

// Correto — ctx como parâmetro de método
func (s *Servico) Executar(ctx context.Context) error

Sempre chame cancelar. Mesmo que o contexto já tenha sido cancelado:

ctx, cancelar := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelar() // sempre, mesmo se cancelado antes

Propague context, nunca crie novos a partir de Background em funções internas:

// Errado — cria contexto independente, ignora cancelamento do chamador
func buscar(id int) {
    ctx := context.Background() // perde o contexto da requisição
    db.QueryContext(ctx, ...)
}

// Correto — propaga o contexto recebido
func buscar(ctx context.Context, id int) {
    db.QueryContext(ctx, ...)
}

Resumo do que foi coberto

Este artigo apresentou o pacote context em profundidade: a interface Context e seus quatro métodos, os contextos raiz Background e TODO, criação de contextos derivados com WithCancel, WithTimeout e WithDeadline, a árvore de contextos e propagação de cancelamento, transporte de valores com WithValue, integração com handlers HTTP e banco de dados, e as boas práticas consolidadas pela comunidade. O próximo artigo explora padrões avançados de concorrência.


Referências e leituras complementares

  • Documentação do pacote context — Referência completa e exemplos oficiais. https://pkg.go.dev/context

  • Go Blog: Go Concurrency Patterns: Context — Artigo original introduzindo o pacote context. https://go.dev/blog/context

  • Go Blog: Contexts and structs — Por que context não deve ser armazenado em structs. https://go.dev/blog/context-and-structs

  • Go by Example: Context — Exemplos práticos e comentados. https://gobyexample.com/context

  • Documentação database/sql — QueryContext — Referência de métodos que aceitam context. https://pkg.go.dev/database/sql#DB.QueryContext

  • OpenTelemetry Go — Como context é usado para propagação de traces em sistemas distribuídos. https://opentelemetry.io/docs/instrumentation/go/

Comentários

Mais em Golang

Ponteiros: conceito, uso e quando evitar
Ponteiros: conceito, uso e quando evitar

Ponteiros s&atilde;o um dos t&oacute;picos que mais intimidam iniciantes vind...

Métodos em Structs: receivers por valor e por ponteiro
Métodos em Structs: receivers por valor e por ponteiro

No artigo anterior, structs foram apresentadas como agrupamentos de dados. M&...

Funções: declaração, múltiplos retornos e variádicas
Funções: declaração, múltiplos retornos e variádicas

Em Go, fun&ccedil;&otilde;es s&atilde;o cidad&atilde;s de primeira classe. El...