O Poder do context.Context em Go: Timeouts, Cancelamento e Propagação

Em Go, poucos pacotes são tão fundamentais e ao mesmo tempo tão mal utilizados quanto context.Context. Ele é a ferramenta padrão para carregar deadlines, sinais de cancelamento e valores compartilhados ao longo da cadeia de chamadas de uma API. Quando bem usado, evita vazamento de goroutines, implementa timeouts elegantemente e torna seu código robusto. Quando mal usado… bem, você já deve ter visto context.Background() dentro de uma função que deveria honrar o cancelamento.

Neste post, vamos explorar o pacote context em profundidade: como criar contextos com timeout/deadline, como cancelar manualmente, como propagar valores (com cuidado) e como evitar as armadilhas mais comuns. Tudo com exemplos práticos para aplicações HTTP, gRPC, banco de dados e filas.

O que é context.Context?

A interface context.Context tem quatro métodos:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
  • Deadline: retorna o momento em que o contexto será cancelado (se houver).
  • Done: canal fechado quando o contexto é cancelado ou atinge o deadline.
  • Err: retorna o motivo do cancelamento (Canceled ou DeadlineExceeded).
  • Value: retorna um valor associado a uma chave (uso muito específico).

Contextos são imutáveis. Para criar um contexto com timeout, você deriva um novo a partir de um pai.

Criando contextos: as funções da biblioteca padrão

FunçãoRetornoUso
context.Background()Contexto vazio, nunca canceladoRaiz da árvore, geralmente em main ou testes
context.TODO()Similar ao BackgroundQuando você ainda não sabe qual contexto usar
context.WithCancel(parent)(ctx, cancel)Cancelamento manual
context.WithTimeout(parent, timeout)(ctx, cancel)Cancelamento automático após tempo
context.WithDeadline(parent, time)(ctx, cancel)Cancelamento em momento absoluto
context.WithValue(parent, key, val)ctxPropaga valores (use com parcimônia)

Todas as funções que retornam um cancelador (cancel func()) devem ser chamadas eventualmente para liberar recursos. Use defer cancel() logo após a criação.

Exemplo 1: Timeout em requisição HTTP

O caso de uso mais comum: você não quer que uma chamada HTTP demore para sempre.

package main

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

func fetchUser(ctx context.Context, id string) (*User, error) {
    // Cria contexto com timeout de 2 segundos
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel() // importante!

    req, err := http.NewRequestWithContext(ctx, "GET", "http://api.example.com/users/"+id, nil)
    if err != nil {
        return nil, err
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        // Se o erro foi por timeout, ctx.Err() será DeadlineExceeded
        return nil, err
    }
    defer resp.Body.Close()

    // processar resposta...
}

func main() {
    ctx := context.Background()
    user, err := fetchUser(ctx, "123")
    if err != nil {
        fmt.Println("Erro:", err)
    }
}

Se a chamada HTTP demorar mais de 2 segundos, o contexto é cancelado e a requisição é abortada. O defer cancel() garante que o contexto seja liberado.

Exemplo 2: Cancelamento manual para operações concorrentes

Às vezes você quer cancelar uma operação quando outra termina primeiro (ex: busca paralela em duas fontes).

func searchFastest(ctx context.Context, query string) string {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    result := make(chan string, 2)

    // Busca em duas fontes simultaneamente
    go func() {
        result <- searchGoogle(ctx, query)
    }()
    go func() {
        result <- searchBing(ctx, query)
    }()

    // Quem responder primeiro vence; o cancel cancela a outra goroutine
    select {
    case res := <-result:
        cancel() // opcional, mas bom para liberar mais rápido
        return res
    case <-ctx.Done():
        return ""
    }
}

Assim que um resultado chegar, cancelamos o contexto, e a outra goroutine (se honrar o contexto) deve interromper seu processamento.

Exemplo 3: Propagando contexto para banco de dados

A maioria dos drivers em Go aceita contexto: db.QueryContext(ctx, ...), db.ExecContext(ctx, ...).

func getOrder(ctx context.Context, db *sql.DB, orderID string) (*Order, error) {
    // Limita a consulta a 1 segundo
    ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
    defer cancel()

    row := db.QueryRowContext(ctx, "SELECT id, total FROM orders WHERE id = $1", orderID)

    var order Order
    if err := row.Scan(&order.ID, &order.Total); err != nil {
        return nil, err
    }
    return &order, nil
}

Se a query demorar mais que 1 segundo, QueryRowContext retorna um erro (geralmente context deadline exceeded).

Exemplo 4: Contexto em servidor HTTP (já é embutido)

A partir do Go 1.7, http.Request já tem um contexto: r.Context(). Use-o para propagar timeouts e cancelamentos.

func slowHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context() // será cancelado se o cliente desconectar ou timeouts do servidor

    select {
    case <-time.After(10 * time.Second):
        w.Write([]byte("Done"))
    case <-ctx.Done():
        log.Println("Cliente cancelou a requisição")
        w.WriteHeader(http.StatusServiceUnavailable)
    }
}

Valor no contexto: use com moderação

context.WithValue permite carregar valores (como request IDs, usuário autenticado, etc.) na árvore de chamadas.

Recomendações oficiais:

  • Use apenas para dados de escopo de requisição (ex: trace ID, user info, deadline).
  • Não use para parâmetros opcionais de função.
  • As chaves devem ser de tipo próprio (ex: type contextKey string), não tipos primitivos (para evitar colisão).
type contextKey string

const userIDKey contextKey = "userID"

func SetUserID(ctx context.Context, userID string) context.Context {
    return context.WithValue(ctx, userIDKey, userID)
}

func GetUserID(ctx context.Context) (string, bool) {
    val := ctx.Value(userIDKey)
    if val == nil {
        return "", false
    }
    userID, ok := val.(string)
    return userID, ok
}

Nunca coloque dados mutáveis (ponteiros que serão modificados) no contexto – isso pode causar data races.

Armadilhas comuns e como evitá-las

1. Esquecer de chamar cancel

Se você cria um contexto com WithCancel, WithTimeout ou WithDeadline e não chama cancel, o contexto vaza recursos (a goroutine interna do contexto fica retida até o cancelamento automático). Sempre use defer cancel().

ctx, cancel := context.WithTimeout(parent, time.Second)
defer cancel() // absolutamente necessário

2. Usar context.Background() onde deveria receber um contexto

Funções que fazem I/O devem aceitar ctx context.Context como primeiro parâmetro. Isso permite que quem chama controle timeouts.

// Ruim
func GetUser(id string) (*User, error)

// Bom
func GetUser(ctx context.Context, id string) (*User, error)

3. Ignorar o erro de cancelamento

Ao chamar funções que aceitam contexto, verifique se o erro foi causado por cancelamento ou timeout.

err := someFunc(ctx)
if errors.Is(err, context.Canceled) {
    // cancelamento intencional (ex: usuário abortou)
} else if errors.Is(err, context.DeadlineExceeded) {
    // timeout
}

4. Propagando contextos que nunca cancelam para operações longas

Se você tem um contexto raiz (ex: de uma goroutine que vive para sempre), não use context.Background() diretamente para operações que devem ser canceladas. Prefira criar um contexto filho com timeout.

5. Misturar contexto e goroutines sem select no Done

Quando você dispara uma goroutine que deve respeitar cancelamento, sempre use select no canal Done.

go func() {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            // faz trabalho
        }
    }
}()

Testando código com context

Simule timeouts e cancelamentos em testes:

func TestFetchUser_Timeout(t *testing.T) {
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
    defer cancel()
    _, err := fetchUser(ctx, "123")
    if err == nil {
        t.Error("esperava erro de timeout, mas não ocorreu")
    }
}

Conclusão

context.Context é uma das ferramentas mais poderosas e idiomáticas de Go. Ele padroniza a forma de lidar com cancelamento, deadlines e valores de escopo de requisição. Domine os padrões: aceite contexto como primeiro parâmetro, use defer cancel() para evitar vazamentos, honre ctx.Done() em goroutines e nunca abuse de WithValue.

Na Jacobus Software, revisamos constantemente o uso de contextos nos códigos de nossos clientes. Um contexto bem aplicado é a diferença entre um sistema que respira e um que engasga sob carga.


⏱️ Seu código Go ignora contextos? Vamos resolver.

Nossos especialistas revisam e refatoram o uso de contextos em sua base de código – evitando vazamentos de goroutine e garantindo timeouts consistentes.

👉 Fale com a Jacobus Software

Rolar para cima