Go

Guia Completo de Context em Go: Cancelamento, Timeout e Propagação de Valores

16 min de leitura

Guia Completo de Context em Go: Cancelamento, Timeout e Propagação de Valores

Context em Go: Cancelamento, Timeout e Propagação de Valores O é 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. 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. O Que é Context e Por Que Usar A Essência do Context Um é uma interface que encapsula um sinal de cancelamento, um deadline (prazo de expiração) e valores de dados imutáveis. A

<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 &quot;fio condutor&quot; 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() &lt;-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 (

&quot;context&quot;

&quot;fmt&quot;

&quot;time&quot;

)

func Worker(ctx context.Context, id int) {

for {

select {

case &lt;-ctx.Done():

fmt.Printf(&quot;Worker %d: Cancelado. Razão: %v\n&quot;, id, ctx.Err())

return

default:

fmt.Printf(&quot;Worker %d: Trabalhando...\n&quot;, 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 &lt;= 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(&quot;Cancelando todos os workers...&quot;)

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 (

&quot;context&quot;

&quot;fmt&quot;

&quot;time&quot;

)

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 &lt;-time.After(5 * time.Second):

// Simula uma operação que leva 5 segundos

fmt.Printf(&quot;%s: Operação concluída\n&quot;, clientName)

return nil

case &lt;-ctx.Done():

// Timeout ou cancelamento externo

return fmt.Errorf(&quot;%s: %w&quot;, clientName, ctx.Err())

}

}

func main() {

// Contexto raiz

ctx, cancel := context.WithCancel(context.Background())

defer cancel()

// Inicia 2 requisições

go func() {

err := ProcessRequest(ctx, &quot;Cliente A&quot;)

fmt.Println(&quot;Cliente A:&quot;, err)

}()

go func() {

err := ProcessRequest(ctx, &quot;Cliente B&quot;)

fmt.Println(&quot;Cliente B:&quot;, 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 &quot;quanto tempo este deve levar?&quot; em vez de &quot;qual é a hora absoluta de expiração?&quot;.</p>

<pre><code class="language-go">package main

import (

&quot;context&quot;

&quot;fmt&quot;

&quot;time&quot;

)

func SlowAPI(ctx context.Context) (string, error) {

// Simula uma API lenta que leva 10 segundos

select {

case &lt;-time.After(10 * time.Second):

return &quot;Sucesso&quot;, nil

case &lt;-ctx.Done():

return &quot;&quot;, ctx.Err()

}

}

func main() {

// Exemplo 1: Timeout curto (vai expirar)

fmt.Println(&quot;=== Teste com Timeout de 2 segundos ===&quot;)

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(&quot;Erro: %v (após %.1f segundos)\n&quot;, err, duration.Seconds())

} else {

fmt.Printf(&quot;Resultado: %s\n&quot;, result)

}

// Exemplo 2: Timeout longo (vai completar)

fmt.Println(&quot;\n=== Teste com Timeout de 15 segundos ===&quot;)

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(&quot;Erro: %v\n&quot;, err)

} else {

fmt.Printf(&quot;Resultado: %s (após %.1f segundos)\n&quot;, 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 (

&quot;context&quot;

&quot;fmt&quot;

&quot;time&quot;

)

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 &lt;-time.After(3 * time.Second):

fmt.Println(&quot;Operação concluída antes do deadline&quot;)

case &lt;-ctx.Done():

fmt.Printf(&quot;Deadline excedido: %v\n&quot;, 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 &quot;saco&quot; 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 (

&quot;context&quot;

&quot;fmt&quot;

)

// Define tipos de chave para evitar colisões

type ContextKey string

const (

RequestIDKey ContextKey = &quot;request_id&quot;

UserIDKey ContextKey = &quot;user_id&quot;

UserNameKey ContextKey = &quot;user_name&quot;

)

func GetUserInfo(ctx context.Context) {

// Recupera valores do contexto

requestID := ctx.Value(RequestIDKey)

userID := ctx.Value(UserIDKey)

userName := ctx.Value(UserNameKey)

fmt.Printf(&quot;RequestID: %v, UserID: %v, UserName: %v\n&quot;,

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, &quot;req-12345&quot;)

ctx = context.WithValue(ctx, UserIDKey, 42)

ctx = context.WithValue(ctx, UserNameKey, &quot;Alice&quot;)

// 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 (

&quot;context&quot;

&quot;fmt&quot;

)

type ContextKey string

const RequestIDKey ContextKey = &quot;request_id&quot;

// Exemplo de má prática

func BadExample(ctx context.Context) {

// ❌ Armazenar slice mutável é perigoso

ctx = context.WithValue(ctx, &quot;items&quot;, []string{&quot;a&quot;, &quot;b&quot;})

}

// Exemplo correto

func GoodExample(ctx context.Context) {

// ✓ Armazenar valor imutável

ctx = context.WithValue(ctx, RequestIDKey, &quot;req-42&quot;)

// Recuperar e usar

if id := ctx.Value(RequestIDKey); id != nil {

fmt.Printf(&quot;Request ID: %s\n&quot;, 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 (

&quot;context&quot;

&quot;fmt&quot;

&quot;log&quot;

&quot;net/http&quot;

&quot;time&quot;

)

type ContextKey string

const RequestIDKey ContextKey = &quot;request_id&quot;

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 &lt;-time.After(3 * time.Second):

fmt.Fprintf(w, &quot;Sucesso! RequestID: %v\n&quot;, ctx.Value(RequestIDKey))

case &lt;-ctx.Done():

w.WriteHeader(http.StatusRequestTimeout)

fmt.Fprintf(w, &quot;Requisição expirou: %v\n&quot;, 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(&quot;req-%d&quot;, 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(&quot;/slow&quot;, SlowEndpoint)

handler := MiddlewareRequestID(mux)

server := &amp;http.Server{

Addr: &quot;:8080&quot;,

Handler: handler,

ReadTimeout: 15 * time.Second,

WriteTimeout: 15 * time.Second,

}

log.Println(&quot;Iniciando servidor em :8080&quot;)

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 (

&quot;context&quot;

&quot;fmt&quot;

&quot;sync&quot;

&quot;time&quot;

)

func Worker(ctx context.Context, id int, jobs &lt;-chan int, wg *sync.WaitGroup) {

defer wg.Done()

for {

select {

case job, ok := &lt;-jobs:

if !ok {

fmt.Printf(&quot;Worker %d: Canal fechado\n&quot;, id)

return

}

fmt.Printf(&quot;Worker %d: Processando job %d\n&quot;, id, job)

time.Sleep(500 * time.Millisecond)

case &lt;-ctx.Done():

fmt.Printf(&quot;Worker %d: Contexto cancelado - %v\n&quot;, 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 &lt;= 3; i++ {

wg.Add(1)

go Worker(ctx, i, jobs, &amp;wg)

}

// Envia jobs

go func() {

for j := 1; j &lt;= 10; j++ {

select {

case jobs &lt;- j:

fmt.Printf(&quot;Job %d enviado\n&quot;, j)

case &lt;-ctx.Done():

fmt.Println(&quot;Context expirou, parando de enviar jobs&quot;)

return

}

}

close(jobs)

}()

// Aguarda conclusão

wg.Wait()

fmt.Println(&quot;Todos os workers finalizados&quot;)

}</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 &amp; Matt Farina (Capítulo sobre Context)</a></li>

</ol>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Como Usar Benchmarks e Profiling em Go: testing.B e pprof na Prática em Produção
Como Usar Benchmarks e Profiling em Go: testing.B e pprof na Prática em Produção

Introdução aos Benchmarks em Go Quando desenvolvemos software em Go, é comum...

Boas Práticas de CQRS e Event Sourcing em Go: Implementação Prática para Times Ágeis
Boas Práticas de CQRS e Event Sourcing em Go: Implementação Prática para Times Ágeis

Entendendo CQRS: O Padrão de Separação de Responsabilidades CQRS significa Co...

Select em Go: Multiplexando Channels e Timeouts: Do Básico ao Avançado
Select em Go: Multiplexando Channels e Timeouts: Do Básico ao Avançado

Introdução ao Select em Go O é uma das construções mais poderosas da linguage...