O que é Context em Go?
O context é um mecanismo da biblioteca padrão de Go para transportar deadlines, sinais de cancelamento e valores request-scoped entre fronteiras de API e goroutines. Definido no pacote context, ele resolve um problema fundamental em sistemas concorrentes: como comunicar que uma operação deve ser interrompida quando o resultado não é mais necessário.
Imagine uma requisição HTTP que dispara consultas ao banco de dados, chamadas a APIs externas e processamento em background. Se o cliente desconecta no meio do caminho, todas essas operações pendentes deveriam parar imediatamente — caso contrário, você desperdiça recursos do servidor. O context é o mecanismo que propaga esse sinal de “pare de trabalhar” por toda a cadeia de chamadas.
O pacote context foi introduzido na biblioteca padrão no Go 1.7, após anos como pacote experimental em golang.org/x/net/context. Hoje, é tão fundamental que praticamente toda interface da biblioteca padrão que faz I/O aceita um context.Context como primeiro parâmetro — de HTTP handlers a operações de banco de dados e chamadas gRPC.
Contextos raiz: Background e TODO
Todo context deriva de um dos dois contextos raiz:
// Contexto vazio — usado como raiz em main(), init() e testes
ctx := context.Background()
// Contexto placeholder — quando não se sabe qual contexto usar ainda
ctx := context.TODO()
Background() é o ponto de partida mais comum. Use-o em main(), na inicialização de serviços e como contexto raiz de requisições quando nenhum outro contexto está disponível.
TODO() existe para marcar no código que você ainda precisa decidir qual contexto usar. Ferramentas de análise estática podem detectar usos de TODO() em produção como potenciais problemas.
WithCancel — cancelamento manual
WithCancel cria um contexto filho que pode ser cancelado explicitamente:
func processarDados(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
defer cancel() // SEMPRE chame cancel para liberar recursos
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("Cancelado:", ctx.Err())
return
default:
// processamento contínuo
fmt.Println("Processando...")
time.Sleep(500 * time.Millisecond)
}
}
}()
// Simular cancelamento após 2 segundos
time.Sleep(2 * time.Second)
cancel() // sinaliza cancelamento
time.Sleep(100 * time.Millisecond) // tempo para goroutine finalizar
}
A regra de ouro: sempre chame cancel(), geralmente com defer. Não chamar cancel() causa vazamento de recursos internos do context, mesmo quando o contexto pai é cancelado.
Padrão com AfterFunc (Go 1.21+)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
stop := context.AfterFunc(ctx, func() {
fmt.Println("Executando cleanup após cancelamento")
})
defer stop()
WithTimeout — cancelamento por tempo
WithTimeout é o padrão mais usado em aplicações web — define um tempo máximo para uma operação:
func buscarUsuario(ctx context.Context, id int) (*Usuario, error) {
// Timeout de 3 segundos para a operação completa
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET",
fmt.Sprintf("https://api.exemplo.com/users/%d", 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 usuário %d", 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
}
Na prática, WithTimeout(ctx, 3*time.Second) é equivalente a WithDeadline(ctx, time.Now().Add(3*time.Second)). Use WithTimeout para durações relativas e WithDeadline para momentos absolutos.
WithDeadline — cancelamento por horário
WithDeadline define um momento exato no tempo como limite:
func processarRelatorio(ctx context.Context) error {
// Deadline: meia-noite de hoje
meianoite := time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour)
ctx, cancel := context.WithDeadline(ctx, meianoite)
defer cancel()
// Verificar quanto tempo resta
if deadline, ok := ctx.Deadline(); ok {
restante := time.Until(deadline)
fmt.Printf("Tempo restante: %v\n", restante)
}
// Operações de longa duração...
return nil
}
Herança de deadlines
Uma propriedade crucial: o deadline efetivo é sempre o menor entre o pai e o filho:
// Pai com timeout de 10 segundos
ctxPai, cancelPai := context.WithTimeout(context.Background(), 10*time.Second)
defer cancelPai()
// Filho com timeout de 5 segundos — efetivo: 5s
ctxFilho5, cancel5 := context.WithTimeout(ctxPai, 5*time.Second)
defer cancel5()
// Filho com timeout de 30 segundos — efetivo: 10s (herda do pai)
ctxFilho30, cancel30 := context.WithTimeout(ctxPai, 30*time.Second)
defer cancel30()
WithValue — transportando valores
WithValue anexa pares chave-valor ao contexto para dados request-scoped:
// Defina tipos custom para chaves (evita colisões)
type chaveContexto string
const (
chaveRequestID chaveContexto = "request_id"
chaveUsuario chaveContexto = "usuario"
)
func middleware(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)
ctx = context.WithValue(ctx, chaveUsuario, "diego")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func handler(w http.ResponseWriter, r *http.Request) {
requestID, ok := r.Context().Value(chaveRequestID).(string)
if !ok {
requestID = "desconhecido"
}
fmt.Fprintf(w, "Request ID: %s", requestID)
}
Regras para WithValue
- Use tipos custom para chaves — nunca use
stringouintdiretamente para evitar colisões entre packages - Apenas para dados request-scoped — request ID, token de autenticação, trace ID
- Nunca para parâmetros de função — se uma função precisa de um valor, passe como parâmetro explícito
- Valores devem ser thread-safe — o context é compartilhado entre goroutines
Context em HTTP handlers
O pacote net/http integra context nativamente:
func meuHandler(w http.ResponseWriter, r *http.Request) {
// O request já carrega um context
ctx := r.Context()
// Adicionar timeout para processamento
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// Passar o context para operações downstream
resultado, err := consultarBanco(ctx)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
http.Error(w, "Timeout", http.StatusGatewayTimeout)
return
}
http.Error(w, "Erro interno", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(resultado)
}
Quando o cliente desconecta, o context do request é cancelado automaticamente. Todas as operações que usam esse context recebem o sinal e podem parar de forma limpa. Isso é essencial para APIs REST e microsserviços em produção.
Context com banco de dados
Toda operação de banco de dados em Go deve receber um context:
func buscarProdutos(ctx context.Context, db *sql.DB, categoria string) ([]Produto, error) {
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx,
"SELECT id, nome, preco FROM produtos WHERE categoria = $1",
categoria)
if err != nil {
return nil, fmt.Errorf("query falhou: %w", err)
}
defer rows.Close()
var produtos []Produto
for rows.Next() {
var p Produto
if err := rows.Scan(&p.ID, &p.Nome, &p.Preco); err != nil {
return nil, err
}
produtos = append(produtos, p)
}
return produtos, rows.Err()
}
Se o context for cancelado durante a query, o driver do banco de dados interrompe a operação. Isso previne queries lentas de monopolizarem conexões do pool — algo crítico para aplicações com PostgreSQL.
Propagação de context
O padrão correto é propagar o context por toda a cadeia de chamadas:
func ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
usuario, err := autenticar(ctx) // propaga
pedidos, err := buscarPedidos(ctx, usuario.ID) // propaga
total := calcularTotal(pedidos) // não precisa (cálculo puro)
err = enviarEmail(ctx, usuario.Email, total) // propaga
}
Regra prática: propague context para qualquer func que faça I/O (rede, disco, banco) ou coordene goroutines. Funções de cálculo puro sem I/O geralmente não precisam de context.
Boas práticas com context
- Context é sempre o primeiro parâmetro —
func Foo(ctx context.Context, ...) - Nunca armazene context em structs — passe-o como parâmetro
- Sempre chame cancel() — use defer logo após a criação
- Não use WithValue como saco de dados — apenas para dados request-scoped
- Verifique ctx.Err() antes de operações custosas — evita trabalho desnecessário
- Prefira context.Background() sobre context.TODO() em código de produção
Para aprofundar em padrões de concorrência com context, explore os padrões de concorrência em Go e entenda como context trabalha com channels e select.
Perguntas frequentes (FAQ)
Qual a diferença entre context.Background() e context.TODO()?
Funcionalmente são idênticos — ambos criam um context vazio que nunca é cancelado. A diferença é semântica: Background() é usado quando você sabe que precisa de um context raiz (em main(), testes, inicialização). TODO() indica que você ainda não decidiu qual context usar — serve como marcador para revisão futura.
Posso armazenar context dentro de um struct?
Não — essa é uma das poucas regras absolutas do Go. O context deve ser passado como primeiro parâmetro de funções, nunca armazenado em campos de structs. Armazenar contextos em structs dificulta o rastreamento de cancelamento e cria riscos de uso de contextos expirados.
Como fazer timeout em chamadas HTTP com context?
Use http.NewRequestWithContext(ctx, method, url, body) para criar um request que respeita o context. Combine com context.WithTimeout para definir o tempo máximo. Se o timeout expirar, o client cancela a requisição e retorna um erro com context.DeadlineExceeded.
Context.WithValue é thread-safe?
Sim. O context é imutável — cada WithValue cria um novo context filho sem modificar o pai. Múltiplas goroutines podem ler valores do mesmo context simultaneamente sem risco de race condition. Porém, os valores armazenados devem ser thread-safe por si mesmos.