O pacote context é uma das ferramentas mais importantes em Go para gerenciar operações de longa duração, como chamadas HTTP, consultas a banco de dados ou qualquer tarefa concorrente. Ele fornece uma maneira padronizada de transmitir sinais de cancelamento, definir prazos e compartilhar valores entre goroutines. Dominar o pacote context é essencial para escrever código Go robusto e eficiente.

Nesta aula, vamos explorar os principais usos do pacote: cancelamento, timeouts e deadlines, propagação de contextos e boas práticas. Cada tópico será acompanhado de exemplos práticos para que você entenda como aplicar esses conceitos no dia a dia.

Cancelamento

O cancelamento é um dos recursos mais poderosos do pacote context. Ele permite que uma goroutine sinalize para outras goroutines que devem parar seu trabalho, liberando recursos rapidamente. Isso é útil, por exemplo, quando um usuário cancela uma requisição ou quando um serviço precisa ser desligado.

Para criar um contexto cancelável, usamos context.WithCancel, que retorna um novo contexto e uma função de cancelamento. Quando a função é chamada, o contexto é cancelado e todos os contextos derivados dele também são cancelados. As goroutines que escutam o canal Done() do contexto podem detectar o cancelamento e agir adequadamente.

package main

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

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // boa prática: garantir cancelamento ao final

    go func() {
        select {
        case <-ctx.Done():
            fmt.Println("Goroutine cancelada")
            return
        case <-time.After(2 * time.Second):
            fmt.Println("Trabalho concluído")
        }
    }()

    time.Sleep(1 * time.Second)
    cancel() // cancela o contexto antes do timeout
    time.Sleep(1 * time.Second)
}

No exemplo acima, a goroutine executa um trabalho que pode levar até 2 segundos. Após 1 segundo, chamamos cancel(), o que faz com que o canal ctx.Done() seja fechado, e a goroutine imediatamente imprime "Goroutine cancelada" e retorna. Sem o cancelamento, ela esperaria os 2 segundos desnecessariamente.

Timeouts e deadlines

Além do cancelamento manual, podemos definir timeouts ou deadlines automáticos usando context.WithTimeout e context.WithDeadline. Um timeout é uma duração máxima para a operação, enquanto um deadline é um momento específico no tempo em que o contexto será cancelado automaticamente.

Esses mecanismos são ideais para evitar que operações fiquem bloqueadas indefinidamente. Por exemplo, ao fazer uma requisição HTTP, podemos definir um timeout de 5 segundos. Se a resposta não chegar nesse período, o contexto é cancelado e podemos tratar o erro adequadamente.

package main

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

func operacaoLenta(ctx context.Context) error {
    select {
    case <-ctx.Done():
        return ctx.Err() // retorna o motivo do cancelamento
    case <-time.After(10 * time.Second):
        fmt.Println("Operação concluída")
        return nil
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    err := operacaoLenta(ctx)
    if err != nil {
        fmt.Println("Erro:", err) // imprime "context deadline exceeded"
    }
}

Neste exemplo, a operação levaria 10 segundos, mas definimos um timeout de 3 segundos. Quando o timeout expira, o contexto é cancelado e ctx.Err() retorna context deadline exceeded. A função então retorna o erro, e podemos tratá-lo adequadamente.

Para usar deadlines, podemos fazer:

deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

Propagação

Uma das características mais importantes do pacote context é a propagação: contextos podem ser aninhados, formando uma árvore. Quando um contexto pai é cancelado, todos os contextos filhos (derivados) também são cancelados. Isso permite que um cancelamento em um nível superior se propague para todas as operações dependentes.

Por exemplo, ao receber uma requisição HTTP, o servidor pode criar um contexto raiz para aquela requisição. Se o cliente desconectar, o contexto pode ser cancelado, e todas as goroutines que realizam tarefas relacionadas (como consultas ao banco, chamadas a APIs externas) serão notificadas e podem parar.

package main

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

func tarefa(ctx context.Context, nome string) {
    select {
    case <-ctx.Done():
        fmt.Printf("%s cancelada\n", nome)
    case <-time.After(2 * time.Second):
        fmt.Printf("%s concluída\n", nome)
    }
}

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

    ctxFilho1, _ := context.WithCancel(ctxPai)
    ctxFilho2, _ := context.WithCancel(ctxPai)

    go tarefa(ctxFilho1, "Tarefa 1")
    go tarefa(ctxFilho2, "Tarefa 2")

    time.Sleep(1 * time.Second)
    cancel() // cancela o pai, que cancela os filhos
    time.Sleep(1 * time.Second)
}

Ao cancelar o contexto pai, ambos os contextos filhos são cancelados automaticamente, e as goroutines imprimem "cancelada". Isso demonstra como a propagação simplifica o gerenciamento de múltiplas operações concorrentes.

Boas práticas

O uso correto do pacote context é fundamental para escrever código Go idiomático e eficiente. Aqui estão algumas boas práticas:

  • Passe context como primeiro parâmetro: Por convenção, o contexto deve ser o primeiro argumento de funções que o utilizam. Exemplo: func minhaFuncao(ctx context.Context, ...).
  • Não armazene contextos em structs: Contextos devem ser passados explicitamente, não armazenados como campos. Isso evita vazamentos e facilita a rastreabilidade.
  • Sempre chame a função de cancelamento: Ao usar WithCancel, WithTimeout ou WithDeadline, use defer cancel() para garantir que o contexto seja cancelado, liberando recursos.
  • Use context.Background() como raiz: Para contextos de alto nível (como no main), use context.Background(). Para testes, use context.TODO() quando o contexto ainda não estiver definido.
  • Não use context para passar parâmetros opcionais: Embora seja possível armazenar valores no contexto com context.WithValue, isso deve ser usado com moderação, apenas para dados de escopo de requisição (como IDs de correlação). Prefira parâmetros explícitos.
  • Verifique ctx.Done() em loops: Em operações que envolvem loops, verifique regularmente se o contexto foi cancelado para evitar trabalho desnecessário.

Exemplo de uso com context.WithValue (uso moderado):

type chave string

const userIDKey chave = "userID"

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := context.WithValue(r.Context(), userIDKey, "123")
    processar(ctx)
}

func processar(ctx context.Context) {
    userID := ctx.Value(userIDKey).(string)
    fmt.Println("User ID:", userID)
}

Referências

Exercícios

  1. Crie uma função que simule uma operação de longa duração (por exemplo, um loop que conta de 1 a 10 com pausas de 1 segundo) e que respeite um contexto cancelável. Use context.WithCancel e cancele o contexto após 3 segundos. A função deve parar imediatamente ao detectar o cancelamento.

    ✓ Resposta:
    package main
    
    import (
        "context"
        "fmt"
        "time"
    )
    
    func operacaoLonga(ctx context.Context) {
        for i := 1; i <= 10; i++ {
            select {
            case <-ctx.Done():
                fmt.Println("Operação cancelada")
                return
            default:
                fmt.Println(i)
                time.Sleep(1 * time.Second)
            }
        }
    }
    
    func main() {
        ctx, cancel := context.WithCancel(context.Background())
        defer cancel()
    
        go operacaoLonga(ctx)
    
        time.Sleep(3 * time.Second)
        cancel()
        time.Sleep(1 * time.Second) // espera a goroutine imprimir
    }
  2. Escreva uma função que faça uma requisição HTTP com timeout de 2 segundos. Se a requisição exceder o timeout, a função deve retornar um erro. Use context.WithTimeout e a biblioteca net/http.

    ✓ Resposta:
    package main
    
    import (
        "context"
        "fmt"
        "net/http"
        "time"
    )
    
    func requisicaoComTimeout(url string) (*http.Response, error) {
        ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
        defer cancel()
    
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
        if err != nil {
            return nil, err
        }
    
        client := &http.Client{}
        resp, err := client.Do(req)
        if err != nil {
            return nil, err
        }
        return resp, nil
    }
    
    func main() {
        resp, err := requisicaoComTimeout("https://httpbin.org/delay/5") // demora 5s
        if err != nil {
            fmt.Println("Erro:", err)
            return
        }
        defer resp.Body.Close()
        fmt.Println("Status:", resp.Status)
    }
  3. Implemente um programa que crie uma árvore de contextos: um contexto pai com timeout de 4 segundos, e dois contextos filhos. Cada filho deve executar uma tarefa que leva 3 segundos. Mostre que, se o pai for cancelado, os filhos também são cancelados.

    ✓ Resposta:
    package main
    
    import (
        "context"
        "fmt"
        "time"
    )
    
    func tarefa(ctx context.Context, nome string) {
        select {
        case <-ctx.Done():
            fmt.Printf("%s cancelada: %v\n", nome, ctx.Err())
        case <-time.After(3 * time.Second):
            fmt.Printf("%s concluída\n", nome)
        }
    }
    
    func main() {
        ctxPai, cancel := context.WithTimeout(context.Background(), 4*time.Second)
        defer cancel()
    
        ctxFilho1, _ := context.WithCancel(ctxPai)
        ctxFilho2, _ := context.WithCancel(ctxPai)
    
        go tarefa(ctxFilho1, "Filho1")
        go tarefa(ctxFilho2, "Filho2")
    
        time.Sleep(5 * time.Second) // espera o timeout do pai
        // Como o pai tem timeout de 4s, ele cancela automaticamente, cancelando os filhos
        // Se quiser cancelar manualmente, chame cancel() antes
    }
  4. Crie uma função que receba um contexto e um número de iterações. A função deve executar um loop que processa cada iteração (por exemplo, imprimir o número) e verificar o contexto a cada iteração. Se o contexto for cancelado, a função deve parar imediatamente e retornar o erro do contexto.

    ✓ Resposta:
    package main
    
    import (
        "context"
        "fmt"
        "time"
    )
    
    func processar(ctx context.Context, iteracoes int) error {
        for i := 0; i < iteracoes; i++ {
            select {
            case <-ctx.Done():
                return ctx.Err()
            default:
                fmt.Println("Processando", i)
                time.Sleep(500 * time.Millisecond)
            }
        }
        return nil
    }
    
    func main() {
        ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
        defer cancel()
    
        err := processar(ctx, 10)
        if err != nil {
            fmt.Println("Erro:", err)
        }
    }
  5. Escreva um programa que use context.WithValue para passar um ID de requisição através de uma cadeia de funções. Crie três funções que se chamam em sequência, cada uma adicionando mais informações ao contexto, e ao final imprima todos os valores.

    ✓ Resposta:
    package main
    
    import (
        "context"
        "fmt"
    )
    
    type chave string
    
    const (
        reqIDKey   chave = "requestID"
        userIDKey  chave = "userID"
        tokenKey   chave = "token"
    )
    
    func funcao1(ctx context.Context) context.Context {
        ctx = context.WithValue(ctx, reqIDKey, "abc123")
        return funcao2(ctx)
    }
    
    func funcao2(ctx context.Context) context.Context {
        ctx = context.WithValue(ctx, userIDKey, "42")
        return funcao3(ctx)
    }
    
    func funcao3(ctx context.Context) context.Context {
        ctx = context.WithValue(ctx, tokenKey, "token-secreto")
        return ctx
    }
    
    func main() {
        ctx := context.Background()
        ctxFinal := funcao1(ctx)
    
        fmt.Println("Request ID:", ctxFinal.Value(reqIDKey))
        fmt.Println("User ID:", ctxFinal.Value(userIDKey))
        fmt.Println("Token:", ctxFinal.Value(tokenKey))
    }