<h2>Context em Go: Cancelamento, Timeout e Propagação de Valores</h2>
<p>O <code>context</code> é um dos pacotes mais importantes da biblioteca padrão de Go. Ele foi introduzido para resolver um problema crítico em aplicações concorrentes: como coordenar o ciclo de vida de operações e garantir que goroutines sejam finalizadas de forma segura e previsível. Sem o context, você teria que usar canais manuais, sinais de cancelamento complexos ou variáveis compartilhadas — tudo aumentando a complexidade e o risco de deadlocks.</p>
<p>Quando você trabalha com requisições HTTP, processamento de dados assíncrono, ou qualquer operação que envolva múltiplas goroutines, o context atua como um "fio condutor" que passa entre as funções, carregando informações sobre cancelamento, timeouts e valores específicos da operação. Entender context é essencial para escrever código Go robusto e profissional.</p>
<h2>O Que é Context e Por Que Usar</h2>
<h3>A Essência do Context</h3>
<p>Um <code>context.Context</code> é uma interface que encapsula um sinal de cancelamento, um deadline (prazo de expiração) e valores de dados imutáveis. A interface é simples, mas poderosa:</p>
<pre><code class="language-go">type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}</code></pre>
<p>O método <code>Done()</code> retorna um canal que será fechado quando o contexto for cancelado ou expirado. O método <code>Err()</code> te diz <em>por que</em> o contexto foi cancelado — se foi por timeout (<code>context.DeadlineExceeded</code>) ou por cancelamento explícito (<code>context.Canceled</code>). O método <code>Value()</code> permite recuperar dados armazenados no contexto.</p>
<p>O ponto-chave é que context é imutável e seguro para usar em múltiplas goroutines simultaneamente. Quando você precisa de um novo contexto com diferentes propriedades (como um timeout mais curto), você não modifica o existente — você cria um novo derivado.</p>
<h3>O Padrão de Design</h3>
<p>A prática recomendada é sempre passar <code>context.Context</code> como primeiro argumento em funções que realizam operações potencialmente bloqueantes. Isso torna explícito que a função respeita cancelamento e timeouts. Veja um exemplo clássico:</p>
<pre><code class="language-go">// Padrão correto: context como primeiro argumento
func FetchUserData(ctx context.Context, userID string) (*User, error) {
// implementação que respeita ctx
}
// Padrão incorreto: context em outro lugar
func FetchUserData(userID string, ctx context.Context) (*User, error) {
// dificulta leitura e é contrário à convenção Go
}</code></pre>
<h2>Cancelamento de Operações</h2>
<h3>Entendendo WithCancel</h3>
<p>O <code>context.WithCancel()</code> retorna um contexto derivado que você pode cancelar manualmente chamando uma função de cancelamento. Isso é útil quando você quer parar uma operação de longa duração sem aguardar um timeout.</p>
<pre><code class="language-go">package main
import (
"context"
"fmt"
"time"
)
func Worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d: Cancelado. Razão: %v\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d: Trabalhando...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
// Cria um contexto que pode ser cancelado
ctx, cancel := context.WithCancel(context.Background())
// Inicia 3 workers
for i := 1; i <= 3; i++ {
go Worker(ctx, i)
}
// Deixa os workers rodar por 2 segundos
time.Sleep(2 * time.Second)
// Cancela todos os workers de uma vez
fmt.Println("Cancelando todos os workers...")
cancel()
// Aguarda um pouco para ver as mensagens
time.Sleep(1 * time.Second)
}</code></pre>
<p>Quando você chama <code>cancel()</code>, o canal <code>Done()</code> é fechado, e todas as goroutines que estão esperando naquele contexto são despertadas. O método <code>Err()</code> retorna <code>context.Canceled</code>. Isso permite interromper operações que estão em loop ou aguardando I/O de forma elegante.</p>
<h3>Padrão Prático de Cancelamento</h3>
<p>Em aplicações reais, frequentemente você quer respeitar cancelamento externo (como quando um cliente desconecta de uma requisição HTTP) e também impor um timeout próprio. Aqui está como combinar essas ideias:</p>
<pre><code class="language-go">package main
import (
"context"
"fmt"
"time"
)
func ProcessRequest(ctx context.Context, clientName string) error {
// Cria um novo contexto derivado com timeout de 3 segundos
// mas que ainda pode ser cancelado pelo ctx pai
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
select {
case <-time.After(5 * time.Second):
// Simula uma operação que leva 5 segundos
fmt.Printf("%s: Operação concluída\n", clientName)
return nil
case <-ctx.Done():
// Timeout ou cancelamento externo
return fmt.Errorf("%s: %w", clientName, ctx.Err())
}
}
func main() {
// Contexto raiz
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Inicia 2 requisições
go func() {
err := ProcessRequest(ctx, "Cliente A")
fmt.Println("Cliente A:", err)
}()
go func() {
err := ProcessRequest(ctx, "Cliente B")
fmt.Println("Cliente B:", err)
}()
time.Sleep(6 * time.Second)
}</code></pre>
<p>Neste exemplo, cada requisição tem seu próprio timeout de 3 segundos, mas ambas podem ser canceladas juntas se você chamar <code>cancel()</code> no contexto pai.</p>
<h2>Timeouts e Deadlines</h2>
<h3>WithTimeout e WithDeadline</h3>
<p>Go oferece duas funções similares: <code>WithTimeout()</code> que recebe uma duração, e <code>WithDeadline()</code> que recebe um tempo absoluto. <code>WithTimeout()</code> é mais comum e prático porque você normalmente pensa em termos de "quanto tempo este deve levar?" em vez de "qual é a hora absoluta de expiração?".</p>
<pre><code class="language-go">package main
import (
"context"
"fmt"
"time"
)
func SlowAPI(ctx context.Context) (string, error) {
// Simula uma API lenta que leva 10 segundos
select {
case <-time.After(10 * time.Second):
return "Sucesso", nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
// Exemplo 1: Timeout curto (vai expirar)
fmt.Println("=== Teste com Timeout de 2 segundos ===")
ctx1, cancel1 := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel1()
start := time.Now()
result, err := SlowAPI(ctx1)
duration := time.Since(start)
if err != nil {
fmt.Printf("Erro: %v (após %.1f segundos)\n", err, duration.Seconds())
} else {
fmt.Printf("Resultado: %s\n", result)
}
// Exemplo 2: Timeout longo (vai completar)
fmt.Println("\n=== Teste com Timeout de 15 segundos ===")
ctx2, cancel2 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel2()
start = time.Now()
result, err = SlowAPI(ctx2)
duration = time.Since(start)
if err != nil {
fmt.Printf("Erro: %v\n", err)
} else {
fmt.Printf("Resultado: %s (após %.1f segundos)\n", result, duration.Seconds())
}
}</code></pre>
<p>O timing é preciso: quando o contexto expira, <code>ctx.Done()</code> é fechado e <code>ctx.Err()</code> retorna <code>context.DeadlineExceeded</code>. Isso permite que suas funções respondam quase imediatamente, sem precisar de timers adicionais.</p>
<h3>Usando Deadlines Absolutos</h3>
<p>Embora menos comum, <code>WithDeadline()</code> é útil quando você tem um timestamp específico até o qual uma operação deve ser concluída:</p>
<pre><code class="language-go">package main
import (
"context"
"fmt"
"time"
)
func ProcessWithDeadline() {
// Define um deadline absoluto: 5 segundos a partir de agora
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
// Tenta uma operação que leva 3 segundos
select {
case <-time.After(3 * time.Second):
fmt.Println("Operação concluída antes do deadline")
case <-ctx.Done():
fmt.Printf("Deadline excedido: %v\n", ctx.Err())
}
}
func main() {
ProcessWithDeadline()
}</code></pre>
<p>Na prática, <code>WithTimeout()</code> é mais legível e é o que você usará 99% das vezes.</p>
<h2>Propagação de Valores através do Context</h2>
<h3>Value() e WithValue()</h3>
<p>O context também funciona como um "saco" imutável de valores. Você pode armazenar dados que precisam ser acessados por múltiplas funções na call stack sem passá-los como parâmetros. Isso é especialmente útil para metadados como IDs de requisição, usuário autenticado, ou configurações específicas.</p>
<pre><code class="language-go">package main
import (
"context"
"fmt"
)
// Define tipos de chave para evitar colisões
type ContextKey string
const (
RequestIDKey ContextKey = "request_id"
UserIDKey ContextKey = "user_id"
UserNameKey ContextKey = "user_name"
)
func GetUserInfo(ctx context.Context) {
// Recupera valores do contexto
requestID := ctx.Value(RequestIDKey)
userID := ctx.Value(UserIDKey)
userName := ctx.Value(UserNameKey)
fmt.Printf("RequestID: %v, UserID: %v, UserName: %v\n",
requestID, userID, userName)
}
func FetchUserDetails(ctx context.Context) {
// A função recebe o contexto já populado
GetUserInfo(ctx)
}
func HandleRequest(ctx context.Context) {
// Adiciona valores ao contexto
ctx = context.WithValue(ctx, RequestIDKey, "req-12345")
ctx = context.WithValue(ctx, UserIDKey, 42)
ctx = context.WithValue(ctx, UserNameKey, "Alice")
// Passa para outras funções
FetchUserDetails(ctx)
}
func main() {
ctx := context.Background()
HandleRequest(ctx)
}</code></pre>
<p>Perceba que <code>WithValue()</code> retorna um novo contexto — nunca modifica o existente. Você pode encadear múltiplas chamadas <code>WithValue()</code> para montar o contexto que precisa. Os valores são recuperados por chave usando <code>Value()</code>, que retorna <code>interface{}</code>, então você precisará fazer type assertion se necessário.</p>
<h3>Boas Práticas com Values</h3>
<p>Existem algumas regras importantes ao trabalhar com valores em context:</p>
<ol>
<li><strong>Use tipos customizados para chaves</strong> — Não use strings diretamente para chaves, pois há risco de colisão. Crie um tipo customizado (como <code>type ContextKey string</code>) em cada package que vai usá-lo.</li>
</ol>
<ol>
<li><strong>Valores devem ser imutáveis</strong> — Não armazene slices ou maps mutáveis, pois isso viola a natureza segura do context. Se precisar de dados compartilhados mutáveis, use mutex e canais.</li>
</ol>
<ol>
<li><strong>Não abuse de values</strong> — Use context.Values para metadados da requisição (IDs, usuário autenticado), não para dados de negócio. Se está passando muitos valores, considere usar uma struct como argumento da função.</li>
</ol>
<pre><code class="language-go">package main
import (
"context"
"fmt"
)
type ContextKey string
const RequestIDKey ContextKey = "request_id"
// Exemplo de má prática
func BadExample(ctx context.Context) {
// ❌ Armazenar slice mutável é perigoso
ctx = context.WithValue(ctx, "items", []string{"a", "b"})
}
// Exemplo correto
func GoodExample(ctx context.Context) {
// ✓ Armazenar valor imutável
ctx = context.WithValue(ctx, RequestIDKey, "req-42")
// Recuperar e usar
if id := ctx.Value(RequestIDKey); id != nil {
fmt.Printf("Request ID: %s\n", id.(string))
}
}
func main() {
GoodExample(context.Background())
}</code></pre>
<h2>Integrando Context em Aplicações Reais</h2>
<h3>Example: HTTP Server com Context</h3>
<p>Um caso de uso prático é integrar context em handlers HTTP para respeitar timeouts de cliente e cancelamento:</p>
<pre><code class="language-go">package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
)
type ContextKey string
const RequestIDKey ContextKey = "request_id"
func SlowEndpoint(w http.ResponseWriter, r *http.Request) {
// O *http.Request já traz um context
ctx := r.Context()
// Adiciona um timeout adicional para esta operação específica
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// Simula processamento longo
select {
case <-time.After(3 * time.Second):
fmt.Fprintf(w, "Sucesso! RequestID: %v\n", ctx.Value(RequestIDKey))
case <-ctx.Done():
w.WriteHeader(http.StatusRequestTimeout)
fmt.Fprintf(w, "Requisição expirou: %v\n", ctx.Err())
}
}
func MiddlewareRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Adiciona um ID único para rastreamento
ctx := context.WithValue(r.Context(), RequestIDKey, fmt.Sprintf("req-%d", time.Now().UnixNano()))
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
func main() {
// Cria um servidor com timeout de leitura
mux := http.NewServeMux()
mux.HandleFunc("/slow", SlowEndpoint)
handler := MiddlewareRequestID(mux)
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
}
log.Println("Iniciando servidor em :8080")
log.Fatal(server.ListenAndServe())
}</code></pre>
<p>Este exemplo mostra como context flui naturalmente em uma aplicação web real. O middleware adiciona um ID de requisição, os handlers respeitam timeouts, e tudo é limpo automaticamente quando a requisição termina.</p>
<h3>Example: Pool de Workers com Context</h3>
<p>Outro padrão comum é usar context para coordenar múltiplos workers que processam tarefas:</p>
<pre><code class="language-go">package main
import (
"context"
"fmt"
"sync"
"time"
)
func Worker(ctx context.Context, id int, jobs <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case job, ok := <-jobs:
if !ok {
fmt.Printf("Worker %d: Canal fechado\n", id)
return
}
fmt.Printf("Worker %d: Processando job %d\n", id, job)
time.Sleep(500 * time.Millisecond)
case <-ctx.Done():
fmt.Printf("Worker %d: Contexto cancelado - %v\n", id, ctx.Err())
return
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
jobs := make(chan int, 10)
var wg sync.WaitGroup
// Inicia 3 workers
for i := 1; i <= 3; i++ {
wg.Add(1)
go Worker(ctx, i, jobs, &wg)
}
// Envia jobs
go func() {
for j := 1; j <= 10; j++ {
select {
case jobs <- j:
fmt.Printf("Job %d enviado\n", j)
case <-ctx.Done():
fmt.Println("Context expirou, parando de enviar jobs")
return
}
}
close(jobs)
}()
// Aguarda conclusão
wg.Wait()
fmt.Println("Todos os workers finalizados")
}</code></pre>
<p>Aqui, o timeout no contexto para todos os workers simultaneamente quando expira. Isso evita que threads fiquem presas ou que você tenha que coordenar cancelamento manualmente.</p>
<h2>Conclusão</h2>
<p>Aprendemos que o <code>context</code> é o mecanismo padrão em Go para coordenar o ciclo de vida de operações concorrentes. Em primeiro lugar, o context permite cancelamento elegante e timeouts precisos através de <code>WithCancel()</code>, <code>WithTimeout()</code> e <code>WithDeadline()</code> — eliminando a necessidade de lógica manual com canais para cada operação. Em segundo lugar, o context propaga valores imutáveis (como IDs de requisição e usuário autenticado) através da call stack de forma segura e eficiente, sem necessidade de passar argumentos extras. Por fim, o padrão de sempre passar context como primeiro argumento de função tornou-se uma convenção Go tão forte que violá-la marca código de baixa qualidade — então sempre respeite esse padrão em suas aplicações.</p>
<h2>Referências</h2>
<ol>
<li><a href="https://pkg.go.dev/context" target="_blank" rel="noopener noreferrer">Context Package - Go Official Documentation</a></li>
<li><a href="https://go.dev/blog/context" target="_blank" rel="noopener noreferrer">Go Concurrency Patterns: Context</a></li>
<li><a href="https://go.dev/doc/effective_go#concurrency" target="_blank" rel="noopener noreferrer">Effective Go - Concurrency</a></li>
<li><a href="https://dave.cheney.net/2017/08/20/context-isnt-for-cancellation" target="_blank" rel="noopener noreferrer">Using Context in Go - Dave Cheney</a></li>
<li><a href="https://www.manning.com/books/go-in-practice" target="_blank" rel="noopener noreferrer">Go in Practice - Matt Butcher & Matt Farina (Capítulo sobre Context)</a></li>
</ol>
<p><!-- FIM --></p>