← Voltar para o blog

Context em Go: Como Usar context.Context Corretamente

Domine context.Context em Go: cancellation, timeouts, deadlines, WithValue, boas práticas em HTTP handlers e banco de dados. Evite os erros mais comuns.

O pacote context é uma das peças mais importantes do ecossistema Go. Ele resolve um problema fundamental em sistemas concorrentes: como sinalizar cancelamento, deadlines e passar metadados entre goroutines de forma segura e padronizada. Se você escreve APIs, acessa bancos de dados ou trabalha com concorrência em Go, dominar context.Context é essencial.

O que é Context?

context.Context é uma interface que carrega deadlines, sinais de cancelamento e valores request-scoped através das fronteiras de uma API. Toda chamada que pode demorar ou ser cancelada deve receber um context.Context como primeiro parâmetro.

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
}
  • Deadline() retorna quando o context vai expirar
  • Done() retorna um canal que é fechado quando o context é cancelado
  • Err() retorna o motivo do cancelamento
  • Value() retorna valores associados ao context

Context Raiz: Background vs TODO

Todo context começa com uma raiz. Go oferece dois:

// Para produção: o context raiz padrão
ctx := context.Background()

// Para código em desenvolvimento onde o context ainda não foi definido
ctx := context.TODO()

context.Background() é o que você usa em 99% dos casos — no main(), na inicialização de serviços e como raiz de novos contexts. Use context.TODO() apenas como marcador temporário enquanto está refatorando código que ainda não recebe context.

WithCancel: Cancelamento Manual

WithCancel cria um context filho que pode ser cancelado manualmente. Quando o context pai é cancelado, os filhos também são.

func processarDados(ctx context.Context) error {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel() // SEMPRE chame cancel para liberar recursos

    resultados := make(chan string, 3)

    // Lançar 3 goroutines para buscar dados
    for i := 0; i < 3; i++ {
        go func(id int) {
            // Simular busca de dados
            time.Sleep(time.Duration(id) * time.Second)
            select {
            case resultados <- fmt.Sprintf("resultado-%d", id):
            case <-ctx.Done():
                return // Context cancelado, encerrar goroutine
            }
        }(i)
    }

    // Pegar apenas o primeiro resultado e cancelar o resto
    select {
    case r := <-resultados:
        fmt.Println("Recebido:", r)
        cancel() // Cancela as outras goroutines
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

A regra de ouro: sempre chame cancel(), mesmo que o context já tenha expirado. O defer cancel() logo após a criação garante que os recursos sejam liberados.

WithTimeout e WithDeadline

WithTimeout e WithDeadline criam contexts que expiram automaticamente. A diferença é sutil: WithTimeout recebe uma duração, WithDeadline recebe um ponto no tempo.

func buscarUsuario(ctx context.Context, id string) (*Usuario, error) {
    // Timeout de 3 segundos para a operação inteira
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, "GET",
        fmt.Sprintf("https://api.exemplo.com/usuarios/%s", id), nil)
    if err != nil {
        return nil, err
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        if ctx.Err() == context.DeadlineExceeded {
            return nil, fmt.Errorf("timeout ao buscar usuario %s", id)
        }
        return nil, err
    }
    defer resp.Body.Close()

    var usuario Usuario
    if err := json.NewDecoder(resp.Body).Decode(&usuario); err != nil {
        return nil, err
    }
    return &usuario, nil
}

Para WithDeadline, o uso é similar mas com um horário absoluto:

// A operação deve terminar até às 23:59:59 de hoje
deadline := time.Now().Truncate(24 * time.Hour).Add(24*time.Hour - time.Second)
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()

WithValue: Passando Valores

WithValue permite associar valores ao context. Use com moderação — context não é um substituto para parâmetros de função.

// Defina tipos privados para chaves — evita colisão
type chaveContexto string

const (
    chaveRequestID chaveContexto = "request_id"
    chaveUserID    chaveContexto = "user_id"
)

func middlewareRequestID(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := uuid.New().String()
        ctx := context.WithValue(r.Context(), chaveRequestID, requestID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func obterRequestID(ctx context.Context) string {
    if id, ok := ctx.Value(chaveRequestID).(string); ok {
        return id
    }
    return "desconhecido"
}

Quando usar WithValue: request ID, user ID de autenticação, trace ID para observabilidade. Quando NÃO usar: parâmetros de negócio, configurações, dependências. Esses devem ser parâmetros explícitos da função.

Context em HTTP Handlers

Em servidores HTTP, cada request já vem com um context que é cancelado quando o cliente desconecta:

func handlerBusca(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    termo := r.URL.Query().Get("q")

    // Adicionar timeout à busca
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    resultados, err := buscarNoIndice(ctx, termo)
    if err != nil {
        if ctx.Err() == context.DeadlineExceeded {
            http.Error(w, "Busca demorou demais", http.StatusGatewayTimeout)
            return
        }
        if ctx.Err() == context.Canceled {
            // Cliente desconectou — não precisa responder
            return
        }
        http.Error(w, "Erro interno", http.StatusInternalServerError)
        return
    }

    json.NewEncoder(w).Encode(resultados)
}

Quando o cliente fecha a conexão, r.Context() é cancelado automaticamente. Isso permite que suas goroutines downstream parem de trabalhar em uma resposta que ninguém vai receber. Para mais detalhes sobre APIs em Go, veja o tutorial de API REST com Go.

Context com Banco de Dados

Todas as operações de banco em Go aceitam context — use-os para evitar queries que travam:

func listarPedidos(ctx context.Context, db *sql.DB, userID int) ([]Pedido, error) {
    // Timeout de 10 segundos para a query
    ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()

    rows, err := db.QueryContext(ctx,
        "SELECT id, produto, valor FROM pedidos WHERE user_id = $1 ORDER BY created_at DESC",
        userID,
    )
    if err != nil {
        return nil, fmt.Errorf("erro ao consultar pedidos: %w", err)
    }
    defer rows.Close()

    var pedidos []Pedido
    for rows.Next() {
        var p Pedido
        if err := rows.Scan(&p.ID, &p.Produto, &p.Valor); err != nil {
            return nil, fmt.Errorf("erro ao ler pedido: %w", err)
        }
        pedidos = append(pedidos, p)
    }
    return pedidos, rows.Err()
}

Se o context for cancelado enquanto a query está rodando, o driver do banco cancela a operação no servidor. Isso é crucial para manter a saúde do PostgreSQL e evitar conexões presas.

Boas Práticas

Siga estas regras para usar context de forma idiomática:

  1. Context é sempre o primeiro parâmetro, nomeado ctx:

    // Correto
    func Processar(ctx context.Context, dados []byte) error
    
    // Errado
    func Processar(dados []byte, ctx context.Context) error
    
  2. Nunca armazene context em uma struct. Passe-o explicitamente em cada chamada de método.

  3. Sempre chame cancel() — use defer cancel() imediatamente após criar o context.

  4. Não passe context nil. Se não sabe qual context usar, use context.TODO().

  5. WithValue é para metadados request-scoped, não para parâmetros de função.

  6. Verifique ctx.Done() em loops longos para permitir cancelamento:

    for _, item := range itens {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
        }
        processar(ctx, item)
    }
    

Anti-Patterns Comuns

Ignorar o context recebido

// ERRADO: cria um context novo, ignorando timeouts do chamador
func BuscarDados(ctx context.Context) error {
    ctx = context.Background() // NÃO faça isso!
    // ...
}

Usar strings como chaves de WithValue

// ERRADO: colisão de chaves entre pacotes
ctx = context.WithValue(ctx, "user_id", 123)

// CORRETO: tipo privado para a chave
type chaveUserID struct{}
ctx = context.WithValue(ctx, chaveUserID{}, 123)

Esquecer de verificar o context em operações longas

Se sua função executa um loop ou múltiplas operações IO, verifique ctx.Done() periodicamente. Caso contrário, o cancelamento só será percebido na próxima chamada que aceita context. Para mais sobre tratamento de erros em Go, confira nosso guia dedicado.

Próximos Passos

Para aprofundar seu conhecimento sobre concorrência e operações assíncronas em Go:

Dominar context.Context é fundamental para escrever código Go robusto e production-ready. Comece aplicando timeouts nas chamadas externas e propague o context por toda a cadeia de chamadas — seu sistema vai agradecer quando uma dependência falhar.