Context em Go
Publicada em · 22 min de leitura
Em sistemas distribuídos e servidores concorrentes, disparar uma tarefa é fácil. O verdadeiro desafio da engenharia de software é saber quando e como fazê-la parar. Antes da introdução do pacote context na biblioteca padrão do Go na versão 1.7, goroutines disparadas para atender requisições web ou consultas de banco de dados frequentemente continuavam rodando mesmo após o usuário ter fechado a aba do navegador ou a conexão ter caído. Esse fenômeno, conhecido como vazamento de goroutines (goroutine leaks), acumulava alocações silenciosas de memória, travava descritores de rede e degradava servidores em produção.
O pacote context resolve esse problema de forma unificada. Ele estabelece uma árvore de contexto pela qual sinais de cancelamento, prazos de expiração (deadlines) e metadados de escopo trafegam por toda a pilha de execução de maneira transparente e coordenada. Esta aula aborda a mecânica completa do pacote, desde a interface fundamental até a implementação estrutural das structs de runtime, os recursos introduzidos nas versões recentes do Go e as armadilhas mais sutis enfrentadas em microsserviços.
| Parte | Seções | O que você vai aprender |
|---|---|---|
| 1. Fundamentos e ciclo de vida | 1 a 3 | O problema do trabalho inútil, a árvore de contextos e a interface Context |
| 2. Anatomia interna do runtime | 4 a 6 | As estruturas emptyCtx, cancelCtx, timerCtx e valueCtx por baixo dos panos |
| 3. Cancelamento e prazos na prática | 7 a 9 | Sincronização via Done(), timeouts defensivos e encerramento em cascata |
| 4. Metadados e segurança de tipos | 10 a 12 | O uso seguro de WithValue, criação de chaves privadas e limites de busca |
| 5. Recursos modernos do ecossistema | 13 a 16 | Cancelamento com causa (Go 1.20+), , WithoutCancel (Go 1.21+) e () nos testes (Go 1.24+) |
| 6. Armadilhas e fechamento | 17 a 20 | Matriz de antipadrões, 5 exercícios com gabarito e glossário técnico |
Pré-requisitos recomendados: Go, Goroutine e Channels.
Parte 1: Fundamentos e ciclo de vida
1. O problema do trabalho inútil antes do context
Imagine um garçom em um restaurante movimentado. Um cliente pede um prato complexo que demora trinta minutos para ser preparado. Dez minutos depois de fazer o pedido, o cliente levanta e vai embora. Se o garçom não avisar a cozinha imediatamente, os cozinheiros continuarão queimando gás, gastando ingredientes nobres e ocupando o forno para entregar um prato que ninguém vai comer nem pagar.
No desenvolvimento de software concorrente, esse desperdício ocorre quando uma requisição HTTP dispara goroutines secundárias para buscar dados em múltiplos serviços. Sem um mecanismo formal de cancelamento, se o cliente original desconectar ou atingir um local, o servidor continuará processando consultas pesadas no PostgreSQL, consumindo banda de rede com APIs externas e ocupando núcleos de CPU desnecessariamente.
A solução canônica do Go para coordenar o ciclo de vida dessas operações é o pacote context. Ele fornece uma via padronizada para que a função pai avise a todas as funções filhas quando uma operação não é mais necessária, permitindo a liberação imediata de recursos.
2. A árvore de derivação e as raízes imutáveis
Um contexto nunca é criado no vácuo. Toda cadeia de execução em Go começa a partir de um nó raiz imutável e ramifica-se em nós filhos à medida que o fluxo atravessa camadas de software, como middlewares, use cases e repositórios.
A biblioteca padrão fornece duas funções para obter contextos raiz:
// Raiz definitiva para fluxos ativos conhecidos (início da aplicação, requisição HTTP raiz)
ctxRoot := context.Background()
// Marcador temporário para indicar código em desenvolvimento ou migração
ctxTodo := context.TODO()
O context.Background() representa a base de qualquer árvore de execução. Ele nunca é cancelado, não possui valores e não expira. Já o context.TODO() possui a mesma implementação física, mas funciona como um lembrete explícito para o time de desenvolvimento de que o contexto adequado ainda precisa ser definido naquele ponto.
A figura ilustra a topologia hierárquica do pacote. A partir do context.Background(), cada função intermediária deriva um novo nó adicionando responsabilidades específicas: ctxAuth transporta a sessão do usuário, ctxReq introduz a capacidade de cancelamento manual, ctxTrace anexa o identificador de correlação e os nós folha (ctxDB e ctxGRPC) estabelecem limites rígidos de tempo para as operações de rede. O aspecto crucial dessa arquitetura é a unidirecionalidade: se o nó ctxReq for cancelado, todos os nós descendentes dele são abortados automaticamente em cascata, enquanto os nós pais permanecem intactos.
3. O contrato da interface Context
Ao contrário de bibliotecas complexas em outras linguagens que implementam dezenas de métodos de orquestração, a interface context.Context em Go é deliberadamente minimalista. Ela é composta por apenas quatro assinaturas:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
O método Deadline informa se a operação possui um horário limite para terminar e qual é esse instante. Caso o contexto não expire por tempo, o booleano ok retorna false.
O método Done retorna um canal somente de leitura de structs vazias (<-chan struct{}). Esse canal funciona como o sinalizador primário de encerramento: enquanto a operação estiver válida, a leitura no canal bloqueia; no momento em que o contexto é cancelado ou atinge o prazo limite, o runtime fecha o canal, liberando instantaneamente todas as goroutines que estavam aguardando em comandos select.
O método Err explica o motivo pelo qual o contexto foi encerrado. Ele retorna nil enquanto o canal Done estiver aberto. Uma vez fechado, passa a retornar um dos dois erros canônicos do pacote: context.Canceled (cancelamento explícito via código) ou context.DeadlineExceeded (tempo limite esgotado).
Por fim, o método Value permite consultar metadados associados ao contexto a partir de uma chave arbitrária, subindo a cadeia de nós até encontrar a correspondência ou atingir a raiz.
Parte 2: Anatomia interna do runtime
4. As quatro estruturas fundamentais do pacote
Para compreender por que o pacote context é tão leve e como ele garante segurança concorrente sem deadlocks internos, é necessário inspecionar as estruturas que implementam a interface Context no código-fonte da linguagem.
O diagrama detalha as quatro implementações concretas mantidas pelo runtime:
-
emptyCtx: Um simples tipo inteiro subjacente (type emptyCtx int) utilizado para representarBackground()eTODO(). Seus métodos são no-op:Done()retornanil,Err()retornanil, eDeadline()retornaok = false. Por ser imutável e estático, ele não exige nenhuma alocação de heap após a inicialização do programa. -
cancelCtx: A espinha dorsal do cancelamento reativo. Possui um campoContextque aponta para o contexto pai, um mutexsync.Mutexpara proteger o estado interno, um canaldone chan struct{}e um mapa de nós filhos (children map[canceler]struct{}). -
timerCtx: Uma estrutura especializada que embutecancelCtxpor composição. Ela adiciona um temporizador físico*time.Timergerenciado pelo runtime e o timestamp exato dodeadline. Quando o cronômetro do sistema dispara, a função interna invocacancelCtx.cancel()de forma assíncrona. -
valueCtx: Um nó puramente associativo que embute o contexto pai e armazena um par chave-valor (key, val any). Ele não possui canais de cancelamento próprios; qualquer chamada aDone()ouErr()é delegada diretamente para o contexto pai referenciado internamente.
5. Como o é propagado
Quando você executa ctx, cancel := context.WithCancel(parent), o runtime não cria um sistema centralizado de publicação de eventos. Em vez disso, ele costura o novo cancelCtx diretamente na árvore genealógica de contextos.
A função interna propagateCancel verifica se o contexto pai também é cancelável. Se o pai for um cancelCtx, o filho se registra dentro do mapa parent.children. Isso estabelece um vínculo forte: no momento em que a função cancel() do pai é executada, ele adquire seu próprio mutex, fecha seu canal done, itera sobre todos os elementos do mapa children invocando o cancelamento recursivo em cada um deles e, por fim, limpa o mapa para permitir que o garbage collector colete as referências.
Se o contexto pai for uma implementação customizada criada por terceiros que não expõe a struct cancelCtx, o Go adota uma estratégia defensiva elegante: ele inicia uma goroutine monitora que fica bloqueada esperando no canal parent.Done(). Caso o pai termine, essa goroutine acorda e cancela o filho imediatamente, preservando a semântica da cascata sob qualquer circunstância.
6. A imutabilidade estrutural e concorrência
Uma regra arquitetural inviolável do pacote context é a imutabilidade externa. Uma vez criado, um nó de contexto nunca sofre mutações em seus valores ou em seu . Quando você “adiciona” um timeout ou um valor a um contexto existente, você não está alterando a struct original; você está criando uma nova casca que envolve o contexto anterior.
Essa decisão de design elimina a necessidade de sincronização manual com mutexes por parte do usuário final. O mesmo objeto ctx pode ser passado com segurança para dezenas de goroutines rodando em núcleos de CPU distintos. Nenhuma delas tem o poder de corromper o estado do contexto ou sobrescrever informações das outras. A única operação de escrita concorrente interna, o cancelamento, é protegida por mutexes estritos no próprio runtime do Go.
Parte 3: Cancelamento e prazos na prática
7. O padrão idiomático com Done e select
O mecanismo fundamental para tornar qualquer algoritmo sensível ao contexto é a inclusão do canal ctx.Done() na estrutura de controle select. Se uma função executa uma computação iterativa pesada ou aguarda I/O de rede, ela deve periodicamente conferir se o cliente ainda deseja o resultado.
Considere o exemplo prático de um gerador de relatórios financeiros que processa lotes de registros:
package main
import (
"context"
"errors"
"fmt"
"time"
)
func ProcessarLotes(ctx context.Context, total int) error {
for i := 1; i <= total; i++ {
select {
case <-ctx.Done():
// O contexto foi encerrado (cancelado ou expirou o prazo)
return fmt.Errorf("processamento interrompido no lote %d: %w", i, ctx.Err())
default:
// Continua a execução normalmente se o canal estiver bloqueado
}
// Simula o processamento pesado de um lote individual
time.Sleep(100 * time.Millisecond)
fmt.Printf("Lote %d processado com sucesso\n", i)
}
return nil
}
O uso do bloco default no select garante que a verificação seja não-bloqueante. Se o canal ctx.Done() ainda estiver aberto, o fluxo segue imediatamente para o processamento do lote. Se o canal foi fechado, a goroutine aborta o laço de repetição e retorna imediatamente, embrulhando o erro retornado por ctx.Err().
O diagrama demonstra a transição física dos componentes durante o cancelamento. Quando a condição de parada é acionada no topo da hierarquia, o canal done fecha. Essa alteração de estado desbloqueia os seletores de todas as goroutines operárias simultaneamente, permitindo que elas finalizem suas rotinas, saiam da pilha de execução e liberem os ponteiros de memória para o coletor de lixo.
8. Timeouts defensivos e controle de deadlines
Em redes reais, servidores externos travam, bancos de dados entram em contenção de locks e pacotes TCP desaparecem no vácuo. Confiar que uma chamada remota vai responder em tempo hábil sem impor um limite formal é uma receita infalível para o colapso operacional da aplicação.
Para estabelecer limites temporais, utilizamos context.WithTimeout ou context.WithDeadline:
// WithTimeout aceita uma duração relativa (ex: 2 segundos a partir de agora)
ctxTimeout, cancelTimeout := context.WithTimeout(ctxPai, 2*time.Second)
defer cancelTimeout()
// WithDeadline aceita um instante absoluto no tempo (ex: 23:59:59 UTC)
limite := time.Now().Add(500 * time.Millisecond)
ctxDeadline, cancelDeadline := context.WithDeadline(ctxPai, limite)
defer cancelDeadline()
O padrão obrigatório em Go é registrar a execução da função de cancelamento através da instrução defer cancel(). Mesmo que a operação conclua antes do tempo e o timeout não dispare, invocar o cancelador no encerramento da função pai desmonta o temporizador interno alocado no runtime, evitando que objetos fiquem presos na memória até o esgotamento natural do cronômetro.
9. Propagação em clientes e servidores HTTP
O servidor web nativo do pacote net/http associa um contexto com cancelamento automático a cada requisição que chega. Se o usuário interrompe o download ou fecha o cliente HTTP, o socket TCP é fechado pelo kernel e o servidor Go encerra automaticamente o contexto daquela requisição (r.Context()).
A figura ilustra o encadeamento de cancelamento em uma arquitetura de serviços. Quando o cliente dispara GET /checkout, o servidor aplica um timeout estrito de 500 milissegundos. Se a consulta ao banco de dados demorar mais do que esse tempo limite, o timerCtx fecha o canal Done(). Essa notificação se propaga instantaneamente para o driver do PostgreSQL (que cancela a query física na conexão) e para o cliente gRPC (que emite um sinal de cancelamento remoto). Em seguida, o servidor responde um código HTTP 504 Gateway Timeout ao usuário, sem deixar nenhuma goroutine zumbi consumindo recursos em segundo plano.
O código a seguir exemplifica como implementar esse encadeamento de forma limpa:
func CheckoutHandler(w http.ResponseWriter, r *http.Request) {
// Aplica um teto defensivo de 500ms sobre a requisição HTTP original
ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
defer cancel()
reqExt, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.pagamentos.com/autorizar", nil)
if err != nil {
http.Error(w, "falha ao construir requisição", http.StatusInternalServerError)
return
}
resp, err := http.DefaultClient.Do(reqExt)
if err != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
http.Error(w, "gateway de pagamentos demorou a responder", http.StatusGatewayTimeout)
return
}
http.Error(w, "falha na comunicação com gateway", http.StatusBadGateway)
return
}
defer resp.Body.Close()
w.WriteHeader(http.StatusOK)
w.Write([]byte("pagamento confirmado"))
}
Parte 4: Metadados e segurança de tipos
10. O propósito e os limites de WithValue
O método context.WithValue foi concebido com uma finalidade muito clara e restrita: transportar dados que pertencem ao ciclo de vida da requisição e que atravessam fronteiras arquiteturais, como identificadores de rastreamento distribuído (trace ID, correlation ID), tokens de autenticação validados em middlewares ou informações de auditoria de tenancy.
O context não deve ser utilizado para passar dependências de negócio, ponteiros para bancos de dados ou parâmetros opcionais de funções. Parâmetros essenciais devem ser explícitos nas assinaturas das funções. Ocultar dados em mapas invisíveis dentro do contexto destrói a clareza do código e inviabiliza a checagem estática de tipos garantida pelo compilador do Go.
11. O perigo de colisões e chaves seguras
A assinatura de WithValue aceita qualquer valor para a chave e para o dado:
func WithValue(parent Context, key, val any) Context
Se dois pacotes diferentes em uma mesma aplicação decidirem usar a string "usuario" ou "tenantID" como chave dentro do contexto, um sobrescreverá o dado do outro silenciosamente. Como o Go permite que bibliotecas externas injetem middlewares no fluxo HTTP, colisões de chaves em strings primitivas representam falhas de segurança e bugs difíceis de rastrear.
A forma idiomática e segura de mitigar esse risco é definir um tipo não exportado baseado em struct vazia ou inteiro dentro do pacote que administra o valor:
package auth
import (
"context"
"errors"
)
// O tipo não exportado garante unicidade absoluta dentro do runtime do Go
type contextKey struct{}
// A chave concreta também é mantida privada
var userKey = contextKey{}
type UserSession struct {
ID string
Roles []string
}
// Injeta a sessão no contexto com tipagem forte na borda
func ContextWithUser(ctx context.Context, session UserSession) context.Context {
return context.WithValue(ctx, userKey, session)
}
// Extrai a sessão com validação do cast de tipos
func UserFromContext(ctx context.Context) (UserSession, error) {
val := ctx.Value(userKey)
if session, ok := val.(UserSession); ok {
return session, nil
}
return UserSession{}, errors.New("sessão de usuário não encontrada no contexto")
}
Ao encapsular o armazenamento por meio de funções exportadas (ContextWithUser e UserFromContext), o restante do sistema consome a funcionalidade sem manipular chaves diretamente e sem risco de cast inválido que cause pânico em runtime.
12. A mecânica de busca linear do valueCtx
Ao contrário de um mapa tradicional (map[any]any), que oferece tempo de consulta em $O(1)$, o valueCtx armazena apenas um único par chave-valor e um ponteiro para o nó pai. Isso significa que uma cadeia de valores no contexto funciona fisicamente como uma lista encadeada simples.
Como demonstrado no diagrama, ao executar ctx.Value(traceKey) no nó mais externo da cadeia, o runtime realiza os seguintes passos:
- Compara a chave informada com a chave do nó atual (
no_user). Se as chaves coincidirem em tipo e valor, o conteúdo é retornado. - Havendo divergência, o runtime segue o ponteiro interno e repete a comparação no nó pai (
no_tenant). - O algoritmo continua subindo recursivamente até encontrar a correspondência no
no_traceou atingir umemptyCtx(onde a busca encerra retornandonil).
A complexidade temporal da busca é estritamente $O(N)$, onde $N$ é o número de camadas de contexto instanciadas. Empilhar dezenas de valores individuais em laços de repetição afeta o desempenho e o consumo de CPU. Se uma requisição precisa transportar múltiplos atributos relacionados, a abordagem correta é agrupá-los em uma única struct e injetar essa struct sob uma única chave.
Parte 5: Recursos modernos do ecossistema
13. Cancelamento semântico com causa (Go 1.20+)
Historicamente, quando um contexto era cancelado, a única informação obtida através de ctx.Err() resumia-se a context.Canceled. Isso criava dificuldades para diagnóstico em sistemas distribuídos: a operação parou porque o cliente fechou a conexão? Porque o pool de conexões esgotou? Ou porque ocorreu uma violação de cota?
O Go 1.20 resolveu essa deficiência introduzindo o cancelamento com causa por meio das funções WithCancelCause e Cause:
package main
import (
"context"
"errors"
"fmt"
)
var (
ErrSaldoInsuficiente = errors.New("saldo insuficiente na conta do cliente")
ErrContaBloqueada = errors.New("conta do cliente temporariamente bloqueada")
)
func main() {
ctx, cancel := context.WithCancelCause(context.Background())
// Simula a tomada de decisão no motor de risco
go func() {
// Encerra o contexto registrando o erro de domínio específico
cancel(ErrSaldoInsuficiente)
}()
<-ctx.Done()
// O erro canônico continua sendo context.Canceled para compatibilidade
fmt.Printf("ctx.Err(): %v\n", ctx.Err())
// O método Cause recupera o erro original registrado na interrupção
origem := context.Cause(ctx)
fmt.Printf("context.Cause(ctx): %v\n", origem)
if errors.Is(origem, ErrSaldoInsuficiente) {
fmt.Println("Tratamento específico: redirecionar para recarga de saldo")
}
}
A função context.Cause retorna o primeiro erro repassado a cancel. Se o cancelamento foi provocado por um estouro de prazo via WithTimeoutCause (adicionado na versão 1.21), Cause revelará se o prazo encerrou naturalmente ou se foi forçado por um evento interno com mensagem customizada.
14. O gancho assíncrono AfterFunc (Go 1.21+)
Antes do Go 1.21, se você precisava reagir ao cancelamento de um contexto dentro de um componente assíncrono (como fechar um canal ou interromper uma conexão de socket bruta), era necessário disparar uma goroutine exclusiva bloqueada em <-ctx.Done(). Criar uma goroutine para cada operação de I/O em servidores que atendem centenas de milhares de conexões gerava sobrecarga mensurável no scheduler.
A introdução de context.AfterFunc simplificou drasticamente essa orquestração:
func ProcessarSocket(ctx context.Context, conn net.Conn) {
// Agenda o fechamento do socket físico assim que o contexto expirar ou for cancelado
stop := context.AfterFunc(ctx, func() {
conn.Close()
})
defer stop()
// Leitura bloqueante do socket que será abortada imediatamente se o contexto terminar
buffer := make([]byte, 1024)
_, err := conn.Read(buffer)
if err != nil {
return
}
}
A função AfterFunc registra um callback para execução assim que o contexto for finalizado. Ela retorna uma função stop que, quando executada antes do disparo do contexto, desregistra o callback de forma segura. Se o contexto já estiver cancelado no momento da chamada, a função fornecida é disparada imediatamente em uma nova goroutine.
15. Desacoplamento de tarefas em background com WithoutCancel
Um cenário frequente em APIs web envolve receber uma requisição HTTP, salvar um registro no banco e disparar uma notificação assíncrona ou auditoria que não pode ser interrompida caso o cliente cancele a chamada no meio do caminho.
Se você simplesmente passar o r.Context() para a goroutine secundária, o cancelamento do cliente HTTP derrubará a gravação de auditoria pela metade. Por outro lado, se você passar context.Background(), perderá os metadados valiosos já injetados, como IDs de trace e dados do usuário logado.
O Go 1.21 introduziu context.WithoutCancel para solucionar esse dilema:
func FinalizarVendaHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Processamento síncrono que respeita o cancelamento da requisição HTTP
err := processarPagamento(ctx)
if err != nil {
http.Error(w, "falha no processamento", http.StatusBadRequest)
return
}
// Cria um contexto que preserva todos os valores (TraceID, UserID),
// mas corta o vínculo com o cancelamento e os prazos da requisição HTTP original
ctxAuditoria := context.WithoutCancel(ctx)
go func() {
// Esta operação roda até o final com segurança, mantendo os logs correlacionados
gravarLogAuditoria(ctxAuditoria)
}()
w.WriteHeader(http.StatusOK)
}
O WithoutCancel retorna uma cópia rasa do contexto que mantém o acesso completo a todos os Value() da árvore, mas ignora completamente qualquer chamada de cancelamento ou prazo estipulado pelo contexto original.
16. Contexto nativo para testes com t.Context() e b.Context() (Go 1.24+)
Em suítes de testes automatizados, praticamente todas as funções modernas recebem ctx context.Context para coordenar cancelamentos e chamadas externas.
Antes do Go 1.24, os desenvolvedores eram obrigados a escrever código repetitivo de configuração no início de cada caso de teste:
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
Além da proliferação de código redundante, essa abordagem manual escondia duas armadilhas críticas:
- Se o teste utilizasse subtestes concorrentes com
t.Parallel(), o gerenciamento de cancelamento tornava-se complexo e propenso a corridas de dados. - Se o programador recorresse diretamente a
context.Background()oucontext.TODO()para economizar linhas de código, goroutines secundárias disparadas pelo código em teste continuavam vivas na memória mesmo após o teste ter falhado prematuramente comt.Fatal()ou chegado ao fim, contaminando os testes subsequentes.
A partir do Go 1.24, os tipos de teste *testing.T, *testing.B e a interface testing.TB ganharam o método nativo .Context().
O antes (Go <= 1.23):
func TestProcessarPedidoLegado(t *testing.T) {
// Boilerplate manual obrigatorio para evitar vazamento de goroutines
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
err := ProcessarPedido(ctx, 123)
if err != nil {
t.Fatalf("falha ao processar pedido: %v", err)
}
}
O depois (Go 1.24+):
package servico_test
import (
"testing"
)
func TestProcessarPedidoModerno(t *testing.T) {
// Contexto nativo gerenciado pelo framework de teste
ctx := t.Context()
// O contexto e cancelado automaticamente assim que o teste encerra,
// garantindo limpeza instantanea de qualquer goroutine pendente
resultado, err := ExecutarOperacaoComContexto(ctx, "dados-de-teste")
if err != nil {
t.Fatalf("operacao falhou: %v", err)
}
if resultado == "" {
t.Error("esperava resultado preenchido")
}
}
func TestSubtestesParalelos(t *testing.T) {
t.Run("caso-rapido", func(t *testing.T) {
t.Parallel()
// Cada subteste possui seu proprio t.Context() isolado
ctx := t.Context()
_ = OperacaoAssincrona(ctx)
})
}
O t.Context() é cancelado imediatamente quando a função de teste retorna ou quando o teste é interrompido por t.FailNow() ou t.Fatal(). O mesmo comportamento aplica-se a benchmarks com b.Context(), eliminando vazamento de recursos e tornando desnecessário o uso de contextos manuais em testes.
Parte 6: Armadilhas e fechamento
17. Matriz de antipadrões e erros clássicos
O uso indevido de contextos é uma das fontes mais comuns de vazamento de memória e inconsistências em microsserviços Go. A tabela a seguir sintetiza as falhas mais frequentes e suas correções canônicas:
| Prática incorreta | Consequência no sistema | Correção idiomática |
|---|---|---|
Armazenar Context em campos de structs | Perde o ciclo de vida da requisição e cria riscos de concorrência com dados obsoletos | Passe ctx explicitamente como o primeiro parâmetro de cada função |
Esquecer de chamar a função cancel() | Desperdiça recursos de temporizadores internos do runtime até a expiração natural | Sempre utilize a instrução defer cancel() imediatamente após criar o contexto |
Usar context.Background() solto em testes unitários | Goroutines secundárias continuam vivas após o teste falhar ou terminar | Utilize t.Context() ou b.Context() nativo do Go 1.24+ |
Usar tipos primitivos (string) como chaves em WithValue | Colisão acidental de chaves entre pacotes e quebra de regras de negócio | Defina um tipo não exportado baseado em struct vazia (type key struct{}) |
| Tratar contexto como se fosse injeção de dependências | Oculta dependências obrigatórias, remove segurança do compilador e dificulta testes | Dependências de infraestrutura devem ser passadas no construtor da struct |
Ignorar o canal Done() em loops computacionais | A goroutine continua executando eternamente mesmo após cancelamento formal | Insira blocos select com case <-ctx.Done(): nas iterações do laço |
18. Exercícios práticos
Exercício 1: Proteção contra cancelamento prematuro
Cenário: Você tem uma função que precisa ler dados de três sensores concorrentes. O sistema deve retornar assim que os três dados estiverem coletados ou se o contexto expirar. Escreva uma função com assinatura ColetarSensores(ctx context.Context) ([]int, error) que dispara três goroutines e encerra o processamento caso o timeout de 200ms seja atingido.
Solução comentada:
package main
import (
"context"
"errors"
"sync"
"time"
)
func ColetarSensores(ctx context.Context) ([]int, error) {
ctxTimeout, cancel := context.WithTimeout(ctx, 200*time.Millisecond)
defer cancel()
resultados := make([]int, 3)
canalResultados := make(chan struct {
idx int
valor int
}, 3)
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(sensorID int) {
defer wg.Done()
// Simula leitura do sensor com tempos variáveis
time.Sleep(time.Duration((sensorID+1)*60) * time.Millisecond)
select {
case canalResultados <- struct {
idx int
valor int
}{idx: sensorID, valor: (sensorID + 1) * 10}:
case <-ctxTimeout.Done():
return
}
}(i)
}
// Goroutine coordenadora para fechar o canal quando todos terminarem
go func() {
wg.Wait()
close(canalResultados)
}()
coletados := 0
for coletados < 3 {
select {
case item, ok := <-canalResultados:
if !ok {
return resultados, nil
}
resultados[item.idx] = item.valor
coletados++
case <-ctxTimeout.Done():
return nil, ctxTimeout.Err()
}
}
return resultados, nil
}
Exercício 2: Isolamento de chaves privadas
Cenário: Crie um pacote com funções utilitárias para anexar e ler um identificador de locatário (TenantID em formato string) em um contexto. Nenhuma outra parte do código fora do pacote deve ser capaz de criar ou colidir com essa chave.
Solução comentada:
package tenant
import (
"context"
"errors"
)
type tenantKey struct{}
var chave = tenantKey{}
var ErrTenantNaoEncontrado = errors.New("identificador de tenant ausente no contexto")
func WithTenantID(ctx context.Context, tenantID string) context.Context {
return context.WithValue(ctx, chave, tenantID)
}
func FromContext(ctx context.Context) (string, error) {
val := ctx.Value(chave)
if tenant, ok := val.(string); ok && tenant != "" {
return tenant, nil
}
return "", ErrTenantNaoEncontrado
}
Exercício 3: Propagação de cancelamento com causa customizada
Cenário: Crie um orquestrador de tarefas em que duas goroutines competem para validar um documento. Se uma delas detectar fraude, ela deve cancelar o contexto de todas as outras usando WithCancelCause, registrando um erro semântico ErrFraudeDetectada.
Solução comentada:
package main
import (
"context"
"errors"
"fmt"
"time"
)
var ErrFraudeDetectada = errors.New("fraude identificada no CPF informado")
func ValidarDocumento(ctx context.Context) error {
ctxCancel, cancel := context.WithCancelCause(ctx)
defer cancel(nil)
go func() {
// Analisador biométrico rápido
time.Sleep(50 * time.Millisecond)
cancel(ErrFraudeDetectada)
}()
go func() {
// Analisador cadastral lento
select {
case <-time.After(200 * time.Millisecond):
fmt.Println("analisador cadastral concluiu")
case <-ctxCancel.Done():
fmt.Println("analisador cadastral abortado")
}
}()
<-ctxCancel.Done()
causa := context.Cause(ctxCancel)
if errors.Is(causa, ErrFraudeDetectada) {
return fmt.Errorf("rejeição na validação: %w", causa)
}
return ctxCancel.Err()
}
Exercício 4: Interrupção limpa de worker pool
Cenário: Implemente um loop consumidor de tarefas que processa itens de um canal e encerra de forma limpa quando o contexto de shutdown da aplicação for acionado.
Solução comentada:
package main
import (
"context"
"fmt"
)
func Worker(ctx context.Context, id int, tarefas <-chan string) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d finalizando de forma limpa por cancelamento: %v\n", id, ctx.Err())
return
case tarefa, ok := <-tarefas:
if !ok {
fmt.Printf("Worker %d finalizando: canal de tarefas encerrado\n", id)
return
}
fmt.Printf("Worker %d executando: %s\n", id, tarefa)
}
}
}
Exercício 5: Encerramento de sockets brutos com AfterFunc
Cenário: Mostre como usar context.AfterFunc para forçar o fechamento imediato de uma conexão net.Conn quando o contexto do cliente for cancelado, impedindo que chamadas Read fiquem bloqueadas indefinidamente no sistema operacional.
Solução comentada:
package main
import (
"context"
"net"
"time"
)
func LerComTimeout(ctx context.Context, conn net.Conn, timeout time.Duration) ([]byte, error) {
ctxLimite, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// Registra o gancho que força o fechamento físico do socket ao estourar o prazo
parar := context.AfterFunc(ctxLimite, func() {
conn.Close()
})
defer parar()
dados := make([]byte, 512)
n, err := conn.Read(dados)
if err != nil {
if ctxLimite.Err() != nil {
return nil, ctxLimite.Err()
}
return nil, err
}
return dados[:n], nil
}
19. Glossário técnico
- Context Tree (Árvore de Contextos): Estrutura hierárquica em que cada nó herda regras de cancelamento e valores de seus predecessores.
- Deadline: Instante fixo no tempo (timestamp absoluto) no qual a execução deve ser obrigatoriamente interrompida.
- Timeout: Período de duração relativa após o qual o contexto é automaticamente encerrado.
- Goroutine Leak: Falha de arquitetura em que uma goroutine permanece bloqueada indefinidamente em memória consumindo recursos sem produzir trabalho útil.
- Cancelation Cascade (Cancelamento em Cascata): Princípio pelo qual o cancelamento de um nó pai fecha automaticamente os canais de todos os seus filhos recursivamente.
- Value Shadowing (Sombreamento de Valor): Comportamento em que um nó filho define uma chave idêntica à de um nó pai, ocultando o valor anterior na resolução de
Value(). - Composition Root: Ponto de entrada de um programa onde a árvore de execução inicial é estabelecida via
context.Background(). - AfterFunc: Mecanismo adicionado no Go 1.21 para registrar callbacks executados de forma assíncrona assim que o contexto associado é cancelado.
- t.Context / b.Context: Método introduzido no Go 1.24 em
testing.TBque fornece um contexto com ciclo de vida atrelado e cancelado automaticamente ao encerramento do teste.
20. Fontes consultadas e leituras recomendadas
- Documentação Oficial da Linguagem Go (pacote
context). Consulta realizada em 18 de outubro de 2026. - Go Blog: “Go Concurrency Patterns: Context”, por Sameer Ajmani.
- Go Reference: “Package context specifications and runtime source code (
src/context/context.go)”. - Effective Go: Diretrizes de concorrência e gerenciamento seguro de ciclos de vida.
- Notas de lançamento oficiais do Go 1.24: novos métodos de contexto em
testing.TB.