O pacote context é uma das ferramentas mais importantes para controle de concorrência em Go. Ele permite que você propague sinais de cancelamento, timeouts e deadlines através de uma cadeia de chamadas, especialmente útil em servidores HTTP, operações de banco de dados e chamadas a APIs externas. Nesta aula, vamos explorar os principais usos do pacote context e como ele pode tornar seu código mais robusto e eficiente.

Contextos são imutáveis e devem ser passados como primeiro argumento em funções que podem ser canceladas ou que possuem prazo. A árvore de contextos é criada a partir de um contexto raiz (context.Background() ou context.TODO()) e derivada com funções como context.WithCancel, context.WithTimeout e context.WithDeadline.

Cancelamento

Cancelamento é a capacidade de sinalizar para goroutines que elas devem parar de executar o trabalho atual. Isso evita desperdício de recursos e permite respostas rápidas a interrupções. O cancelamento é feito através do canal Done() do contexto, que é fechado quando a função de cancelamento é chamada.

Exemplo prático: uma operação que pode ser cancelada pelo usuário ou por outro componente. Vamos criar um contexto cancelável e usar select para ouvir o cancelamento.

package main

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

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // importante: sempre chamar cancel para liberar recursos

    go func() {
        time.Sleep(2 * time.Second)
        cancel() // cancela o contexto após 2 segundos
    }()

    select {
    case <-time.After(5 * time.Second):
        fmt.Println("Operação concluída")
    case <-ctx.Done():
        fmt.Println("Operação cancelada:", ctx.Err())
    }
}

No exemplo, a goroutine cancela o contexto após 2 segundos. O select espera até 5 segundos ou até o cancelamento. Como o cancelamento ocorre primeiro, a saída será "Operação cancelada: context canceled".

Timeouts e deadlines

Timeouts e deadlines são variações do cancelamento que ocorrem automaticamente após um período de tempo. context.WithTimeout cria um contexto que será cancelado após a duração especificada. context.WithDeadline define um ponto absoluto no tempo para o cancelamento.

Exemplo de timeout: uma requisição HTTP que deve falhar se demorar mais que 1 segundo.

package main

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

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

    // Simula uma operação longa
    select {
    case <-time.After(3 * time.Second):
        fmt.Println("Operação concluída")
    case <-ctx.Done():
        fmt.Println("Timeout:", ctx.Err())
    }
}

A saída será "Timeout: context deadline exceeded" porque a operação leva 3 segundos, mas o timeout é de 1 segundo.

Deadlines são úteis quando você tem um prazo absoluto, como uma data específica. Exemplo:

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

Propagação

Contextos são projetados para serem propagados através da árvore de chamadas. Quando você cria um contexto filho a partir de um pai, o cancelamento do pai propaga-se para todos os filhos. Isso permite que um sinal de cancelamento em um nível superior cancele todas as operações descendentes.

Exemplo: uma função que faz duas chamadas concorrentes e cancela ambas se uma falhar.

package main

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

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

    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        defer wg.Done()
        operacao(ctx, "A", 3*time.Second)
    }()

    go func() {
        defer wg.Done()
        operacao(ctx, "B", 1*time.Second)
    }()

    // Cancela após 2 segundos
    time.Sleep(2 * time.Second)
    cancel()

    wg.Wait()
    fmt.Println("Fim")
}

func operacao(ctx context.Context, nome string, duracao time.Duration) {
    select {
    case <-time.After(duracao):
        fmt.Printf("Operação %s concluída\n", nome)
    case <-ctx.Done():
        fmt.Printf("Operação %s cancelada: %v\n", nome, ctx.Err())
    }
}

Neste exemplo, a operação B termina em 1 segundo antes do cancelamento, enquanto a operação A é cancelada após 2 segundos. Ambas recebem o sinal de cancelamento porque o contexto pai foi cancelado.

Boas práticas

Ao usar contextos, siga estas boas práticas:

  • Passe o contexto como primeiro argumento em funções que podem ser canceladas ou que precisam de deadline.
  • Nunca armazene contextos em structs; passe-os explicitamente.
  • Sempre chame a função de cancelamento retornada por WithCancel, WithTimeout ou WithDeadline para liberar recursos, geralmente com defer cancel().
  • Use context.Background() para contextos raiz e context.TODO() quando você ainda não sabe qual contexto usar.
  • Evite passar nil como contexto; use context.TODO() se necessário.
  • Não modifique o contexto depois de criá-lo; contextos são imutáveis.
  • Valores associados ao contexto (com context.WithValue) devem ser usados com moderação, apenas para dados de escopo de requisição, como IDs de tracing.

Exemplo de uso correto com valores:

type key string

const userIDKey key = "userID"

func handler(ctx context.Context) {
    userID := ctx.Value(userIDKey).(string)
    // usa userID
}

func main() {
    ctx := context.WithValue(context.Background(), userIDKey, "12345")
    handler(ctx)
}

Referências

Exercícios

  1. Crie uma função que receba um contexto e um nome de arquivo, e simule a leitura de um arquivo que leva 2 segundos. Se o contexto for cancelado antes, a função deve retornar imediatamente com um erro.

    ✓ Resposta:
    func readFile(ctx context.Context, filename string) ([]byte, error) {
        select {
        case <-time.After(2 * time.Second):
            // Simula leitura
            return []byte("conteúdo"), nil
        case <-ctx.Done():
            return nil, ctx.Err()
        }
    }
    
  2. Escreva um programa que faz duas requisições HTTP concorrentes com timeout de 1 segundo cada. Use contextos com timeout. Se uma requisição falhar por timeout, a outra deve ser cancelada.

    ✓ Resposta:
    package main
    
    import (
        "context"
        "fmt"
        "net/http"
        "sync"
        "time"
    )
    
    func main() {
        ctx, cancel := context.WithCancel(context.Background())
        defer cancel()
    
        var wg sync.WaitGroup
        wg.Add(2)
    
        go func() {
            defer wg.Done()
            reqCtx, reqCancel := context.WithTimeout(ctx, 1*time.Second)
            defer reqCancel()
            req, _ := http.NewRequestWithContext(reqCtx, "GET", "http://example.com", nil)
            _, err := http.DefaultClient.Do(req)
            if err != nil {
                fmt.Println("Erro na requisição 1:", err)
                cancel() // cancela a outra
            }
        }()
    
        go func() {
            defer wg.Done()
            reqCtx, reqCancel := context.WithTimeout(ctx, 1*time.Second)
            defer reqCancel()
            req, _ := http.NewRequestWithContext(reqCtx, "GET", "http://example.org", nil)
            _, err := http.DefaultClient.Do(req)
            if err != nil {
                fmt.Println("Erro na requisição 2:", err)
                cancel()
            }
        }()
    
        wg.Wait()
    }
    
  3. Implemente uma função que executa uma tarefa em loop até que o contexto seja cancelado. A função deve processar itens de um canal e parar quando o contexto for cancelado, fechando o canal.

    ✓ Resposta:
    func worker(ctx context.Context, jobs <-chan int, results chan<- int) {
        for {
            select {
            case <-ctx.Done():
                close(results)
                return
            case job, ok := <-jobs:
                if !ok {
                    close(results)
                    return
                }
                // processa job
                results <- job * 2
            }
        }
    }
    
  4. Crie um servidor HTTP que usa contextos para cancelar requisições longas. Use http.TimeoutHandler ou implemente manualmente com context.WithTimeout.

    ✓ Resposta:
    package main
    
    import (
        "context"
        "fmt"
        "net/http"
        "time"
    )
    
    func slowHandler(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        select {
        case <-time.After(5 * time.Second):
            fmt.Fprintln(w, "Resposta lenta")
        case <-ctx.Done():
            http.Error(w, "Request cancelled", http.StatusRequestTimeout)
        }
    }
    
    func main() {
        handler := http.HandlerFunc(slowHandler)
        timeoutHandler := http.TimeoutHandler(handler, 2*time.Second, "Timeout")
        http.ListenAndServe(":8080", timeoutHandler)
    }
    
  5. Escreva uma função que propaga valores através do contexto (como um ID de correlação) e os utiliza em funções aninhadas. Demonstre o uso de context.WithValue.

    ✓ Resposta:
    package main
    
    import (
        "context"
        "fmt"
    )
    
    type key string
    
    const traceIDKey key = "traceID"
    
    func main() {
        ctx := context.WithValue(context.Background(), traceIDKey, "abc123")
        process(ctx)
    }
    
    func process(ctx context.Context) {
        traceID := ctx.Value(traceIDKey).(string)
        fmt.Println("Trace ID:", traceID)
        subProcess(ctx)
    }
    
    func subProcess(ctx context.Context) {
        traceID := ctx.Value(traceIDKey).(string)
        fmt.Println("Subprocess trace ID:", traceID)
    }