O pacote context é uma das ferramentas mais importantes para escrever aplicações web robustas em Go. Ele permite que você carregue informações de escopo, como prazos, cancelamentos e valores, através da cadeia de chamadas de funções, especialmente em servidores HTTP. Nesta aula, vamos explorar como usar context em handlers web, desde a propagação básica até o gerenciamento de timeouts e valores. Você verá como isso melhora a eficiência, o controle e a segurança das suas aplicações.

Entender context é essencial para construir serviços que respondam rapidamente, evitem vazamentos de recursos e sejam fáceis de manter. Vamos começar com uma visão geral e depois mergulhar em cada aspecto com exemplos práticos.

Propagando context

Em um handler web, você recebe um http.Request que contém um contexto associado, acessível via r.Context(). Esse contexto é derivado do contexto da requisição e carrega informações como o prazo de cancelamento (se houver) e quaisquer valores definidos pelo middleware. A propagação correta desse contexto para funções que fazem trabalho pesado, como consultas ao banco de dados ou chamadas a APIs externas, é crucial para que essas operações possam ser canceladas quando o cliente desconectar ou o servidor decidir interromper.

Para propagar o contexto, você deve passá-lo explicitamente como primeiro argumento para funções que o aceitam. Por exemplo, ao usar o pacote database/sql, as chamadas como QueryContext e ExecContext recebem um contexto. Vamos ver um exemplo de um handler que usa o contexto para consultar um banco de dados:

package main

import (
    "context"
    "database/sql"
    "net/http"
)

func getUserHandler(db *sql.DB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        userID := r.URL.Query().Get("id")
        if userID == "" {
            http.Error(w, "missing id", http.StatusBadRequest)
            return
        }

        var name string
        err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = ?", userID).Scan(&name)
        if err != nil {
            if err == sql.ErrNoRows {
                http.NotFound(w, r)
            } else {
                http.Error(w, "internal error", http.StatusInternalServerError)
            }
            return
        }

        w.Write([]byte("User: " + name))
    }
}

Note que passamos ctx para QueryRowContext. Isso garante que, se o contexto for cancelado (por exemplo, quando o cliente desconecta), a consulta será interrompida e a conexão com o banco de dados não ficará presa. Sem essa propagação, a consulta continuaria até terminar, desperdiçando recursos.

Cancelamento de requisições

O cancelamento de requisições é uma das principais vantagens do uso de context. Quando um cliente HTTP cancela uma solicitação (por exemplo, fechando o navegador), o contexto da requisição é cancelado automaticamente. Isso permite que você interrompa operações em andamento, evitando trabalho desnecessário e liberando recursos.

Para lidar com o cancelamento, você pode verificar o canal Done() do contexto ou usar funções que já aceitam contexto e respondem ao cancelamento. Vamos ver um exemplo de um handler que simula um trabalho longo e verifica se o contexto foi cancelado:

func slowHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    select {
    case <-time.After(5 * time.Second):
        w.Write([]byte("Done after 5 seconds"))
    case <-ctx.Done():
        // Cliente desconectou ou contexto cancelado
        http.Error(w, "request canceled", http.StatusRequestTimeout)
    }
}

Neste exemplo, usamos select para aguardar tanto o tempo de 5 segundos quanto o cancelamento do contexto. Se o contexto for cancelado antes, respondemos com um erro. Em um cenário real, você pode usar essa técnica para interromper chamadas a APIs externas, processamento de arquivos, etc.

Além disso, é importante lembrar que você também pode cancelar o contexto manualmente usando context.WithCancel, mas no caso de handlers web, o cancelamento automático é a norma.

Valores

O contexto também pode carregar valores que são acessíveis ao longo da cadeia de chamadas. Isso é útil para passar informações como IDs de usuário autenticado, tokens de rastreamento, ou configurações específicas da requisição. No entanto, a documentação oficial recomenda usar valores apenas para dados de escopo de requisição, não para parâmetros opcionais de funções.

Para armazenar um valor no contexto, você usa context.WithValue. Para recuperá-lo, usa ctx.Value(key). É importante usar chaves de um tipo definido por você para evitar colisões. Vamos ver um exemplo de como um middleware pode adicionar um ID de usuário ao contexto:

type contextKey string

const userIDKey contextKey = "userID"

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Simula autenticação
        userID := "12345"
        ctx := context.WithValue(r.Context(), userIDKey, userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func handler(w http.ResponseWriter, r *http.Request) {
    userID := r.Context().Value(userIDKey)
    if userID == nil {
        http.Error(w, "unauthorized", http.StatusUnauthorized)
        return
    }
    w.Write([]byte("User ID: " + userID.(string)))
}

No exemplo, definimos um tipo contextKey e uma constante userIDKey para evitar conflitos. O middleware injeta o valor no contexto, e o handler o recupera. Lembre-se de que os valores são imutáveis e devem ser usados apenas para dados que não mudam durante a requisição.

É importante notar que usar valores para passar parâmetros de função é considerado má prática. O contexto deve ser usado para dados de infraestrutura, não para lógica de negócio.

Timeouts

Timeouts são essenciais para evitar que um handler fique preso por muito tempo, especialmente quando dependemos de serviços externos. Você pode definir um timeout para a requisição inteira ou para operações específicas. O contexto permite que você crie um contexto com prazo usando context.WithTimeout ou context.WithDeadline.

Quando você define um timeout, o contexto é cancelado automaticamente quando o prazo expira. Isso é útil para limitar o tempo de execução de uma operação. Vamos ver um exemplo de um handler que define um timeout de 2 segundos para uma chamada a uma API externa:

func externalAPICall(ctx context.Context) (string, error) {
    // Simula uma chamada lenta
    select {
    case <-time.After(3 * time.Second):
        return "response", nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()

    result, err := externalAPICall(ctx)
    if err != nil {
        if err == context.DeadlineExceeded {
            http.Error(w, "timeout", http.StatusGatewayTimeout)
        } else {
            http.Error(w, "error", http.StatusInternalServerError)
        }
        return
    }
    w.Write([]byte(result))
}

Aqui, criamos um contexto com timeout de 2 segundos. A função externalAPICall respeita o contexto e retorna um erro se o prazo expirar. No handler, tratamos o erro de timeout de forma adequada.

Você também pode definir timeouts no servidor HTTP, como ReadTimeout e WriteTimeout, mas o contexto permite um controle mais granular.

Boas práticas e observações finais

Ao usar context em handlers web, siga estas boas práticas:

  • Sempre propague o contexto da requisição para funções que aceitam contexto.
  • Não armazene contextos em structs; passe-os explicitamente.
  • Use valores de contexto apenas para dados de escopo de requisição, como IDs de usuário, e evite usá-los para parâmetros de negócio.
  • Defina timeouts para operações que podem ser lentas, como chamadas de rede ou I/O.
  • Verifique o canal Done() do contexto em loops longos ou operações assíncronas para responder ao cancelamento.

Dominar o uso de context é um diferencial para escrever aplicações Go robustas e eficientes. Pratique com os exercícios abaixo para consolidar o conhecimento.

Exercícios

  1. Escreva um handler que use o contexto da requisição para buscar dados de um banco de dados. Se a requisição for cancelada, a consulta deve ser interrompida.
  2. ✓ Resposta:
    func handler(db *sql.DB) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
            ctx := r.Context()
            id := r.URL.Query().Get("id")
            var name string
            err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = ?", id).Scan(&name)
            if err != nil {
                if err == context.Canceled {
                    http.Error(w, "request canceled", 499)
                } else {
                    http.Error(w, "error", http.StatusInternalServerError)
                }
                return
            }
            w.Write([]byte(name))
        }
    }
  3. Crie um middleware que adicione um valor de rastreamento (trace ID) ao contexto e um handler que o recupere.
  4. ✓ Resposta:
    type ctxKey string
    const traceIDKey ctxKey = "traceID"
    
    func middleware(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            traceID := r.Header.Get("X-Trace-ID")
            if traceID == "" {
                traceID = uuid.New().String()
            }
            ctx := context.WithValue(r.Context(), traceIDKey, traceID)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
    
    func handler(w http.ResponseWriter, r *http.Request) {
        traceID := r.Context().Value(traceIDKey).(string)
        w.Write([]byte("Trace: " + traceID))
    }
  5. Implemente um handler que defina um timeout de 1 segundo para uma tarefa que leva 2 segundos. Trate o caso de timeout corretamente.
  6. ✓ Resposta:
    func handler(w http.ResponseWriter, r *http.Request) {
        ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
        defer cancel()
    
        select {
        case <-time.After(2 * time.Second):
            w.Write([]byte("done"))
        case <-ctx.Done():
            http.Error(w, "timeout", http.StatusGatewayTimeout)
        }
    }
  7. Escreva uma função que execute uma operação em uma goroutine e use o contexto para cancelá-la se o contexto for cancelado.
  8. ✓ Resposta:
    func doWork(ctx context.Context) {
        go func() {
            select {
            case <-time.After(3 * time.Second):
                fmt.Println("work done")
            case <-ctx.Done():
                fmt.Println("work canceled")
            }
        }()
    }
  9. Crie um handler que use context.WithValue para armazenar um objeto de configuração e o recupere em uma função auxiliar.
  10. ✓ Resposta:
    type config struct {
        DB *sql.DB
    }
    
    func handler(w http.ResponseWriter, r *http.Request) {
        cfg := &config{DB: db}
        ctx := context.WithValue(r.Context(), "config", cfg)
        useConfig(ctx)
    }
    
    func useConfig(ctx context.Context) {
        cfg := ctx.Value("config").(*config)
        // usa cfg.DB
    }

Referências