context.Context é uma das peças mais importantes para escrever Go confiável em produção. Ele não é apenas um parâmetro que aparece no começo das funções. Ele carrega cancelamento, timeout, deadline e valores de escopo de requisição ao longo de chamadas HTTP, queries no banco, workers, filas, clients externos e rotinas concorrentes.
Quando um usuário desiste da requisição, uma chamada externa estoura o tempo esperado, um deploy precisa encerrar o processo ou um worker deve parar de consumir jobs, alguém precisa avisar o restante do código. Sem context, cada função continua trabalhando como se nada tivesse acontecido: query pendurada, goroutine vazando, request HTTP esperando indefinidamente, log sem correlação e shutdown travado.
Este guia mostra como usar context.Context em APIs e serviços Go: quando criar timeout, como propagar cancelamento, onde evitar context.Background(), como integrar com banco de dados, HTTP client, worker pool, logs e testes. Se você está montando uma aplicação web, leia também API REST em Go, observabilidade em Go e worker pool em Go.
O que context.Context resolve
context.Context resolve um problema de coordenação: uma operação começa em um ponto do sistema e atravessa várias camadas. Em uma API, por exemplo, a requisição chega no handler, chama serviço de domínio, consulta banco, chama uma API externa, publica evento e grava logs. Se o cliente desconecta ou o limite de tempo expira, todas essas camadas precisam saber.
O pacote context oferece quatro ideias principais:
- Cancelamento: um sinal dizendo que a operação não deve continuar.
- Deadline: um horário máximo para a operação terminar.
- Timeout: uma duração máxima, que vira deadline internamente.
- Values: dados pequenos e transversais, como
request_id, quando realmente necessários.
O mais importante é entender que context não mata goroutine à força. Ele emite um sinal. Seu código precisa observar ctx.Done() ou chamar APIs que já respeitam ctx, como http.NewRequestWithContext, db.QueryContext e métodos de clients modernos.
A regra prática de assinatura
Em código Go de produção, funções que fazem I/O, esperam, bloqueiam, consultam banco, chamam rede ou executam trabalho potencialmente longo devem receber context.Context como primeiro argumento:
func (s *UserService) FindUser(ctx context.Context, id string) (*User, error) {
return s.repo.FindByID(ctx, id)
}
Essa convenção torna a propagação explícita. O handler recebe um contexto da requisição HTTP e passa adiante. O serviço não inventa um contexto novo. O repositório usa o mesmo contexto na query. Se a requisição for cancelada, a query também pode ser cancelada.
Evite armazenar context.Context dentro de structs. Contexto é por operação, não por componente. Um UserService vive muito mais que uma requisição; guardar o contexto nele mistura ciclos de vida diferentes e costuma gerar bugs sutis.
Timeout no handler HTTP
O servidor HTTP do Go já cria um contexto por requisição. Você acessa com r.Context(). Quando o cliente desconecta, esse contexto é cancelado. Para impor um limite interno menor, derive um contexto com timeout:
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
user, err := h.users.FindUser(ctx, r.PathValue("id"))
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "tempo limite excedido", http.StatusGatewayTimeout)
return
}
if errors.Is(err, context.Canceled) {
return
}
http.Error(w, "erro interno", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
O defer cancel() é obrigatório mesmo quando o timeout vai expirar sozinho. Ele libera timers e recursos associados ao contexto derivado. Em handlers curtos, o custo é pequeno; em servidores com muito tráfego, esquecer cancelamento vira desperdício acumulado.
Não use o mesmo timeout para tudo. Um endpoint de autocomplete pode ter 200 ms. Um relatório pode ter 10 segundos. Uma chamada interna crítica pode ter 800 ms. Timeouts devem refletir experiência do usuário, custo operacional e SLO do serviço chamado.
Banco de dados com QueryContext
O pacote database/sql e drivers modernos respeitam contexto nas operações com sufixo Context:
func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
row := r.db.QueryRowContext(ctx, `
SELECT id, name, email
FROM users
WHERE id = $1
`, id)
var user User
if err := row.Scan(&user.ID, &user.Name, &user.Email); err != nil {
return nil, err
}
return &user, nil
}
Isso é mais seguro que criar timeout apenas no banco ou apenas no load balancer. A requisição tem um orçamento total. Cada camada consome parte desse orçamento. Se o contexto expira, a query deve parar e devolver erro, em vez de continuar ocupando conexão no pool.
Em sistemas com PostgreSQL, MySQL ou Redis, combine context com configuração correta de pool: máximo de conexões, tempo de vida, idle timeout e métricas. context ajuda a cancelar trabalho, mas não substitui dimensionamento do pool. Para a base de persistência, veja também Go com PostgreSQL e migrations em Go.
HTTP client com contexto
Chamadas externas sem timeout são um dos bugs mais comuns em APIs. O cliente remoto pode ficar lento, o DNS pode falhar, a rede pode oscilar ou o provedor pode aceitar conexão sem responder. Em Go, crie a requisição com contexto:
func (c *BillingClient) GetInvoice(ctx context.Context, id string) (*Invoice, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/invoices/"+id, nil)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("billing retornou status %d", resp.StatusCode)
}
var invoice Invoice
if err := json.NewDecoder(resp.Body).Decode(&invoice); err != nil {
return nil, err
}
return &invoice, nil
}
Além do contexto por operação, configure http.Client com timeout e transporte adequado. O contexto expressa o orçamento daquela chamada. O client protege contra problemas de conexão e leitura em nível mais baixo. Em serviços críticos, também entram retry com backoff, circuit breaker, rate limiting e observabilidade.
Cancelamento em goroutines e workers
Goroutines longas precisam observar cancelamento. Um loop que lê fila, processa jobs ou faz polling deve parar quando ctx.Done() fecha:
func worker(ctx context.Context, jobs <-chan Job, logger *slog.Logger) {
for {
select {
case <-ctx.Done():
logger.Info("worker encerrado", slog.Any("err", ctx.Err()))
return
case job, ok := <-jobs:
if !ok {
return
}
if err := processJob(ctx, job); err != nil {
logger.Error("falha ao processar job", slog.String("job_id", job.ID), slog.Any("err", err))
}
}
}
}
Esse padrão é essencial para shutdown gracioso. Quando o processo recebe SIGTERM, o main cancela o contexto raiz, o servidor HTTP para de aceitar novas requisições e os workers terminam o que for seguro terminar. Sem isso, deploys ficam lentos, jobs duplicam ou goroutines ficam presas em chamadas externas.
Use cuidado ao passar o mesmo contexto cancelado para retries. Se a operação original expirou, um retry usando o mesmo contexto vai falhar imediatamente. Isso pode ser correto em um handler HTTP, porque o usuário já esperou demais. Em um worker assíncrono, talvez seja melhor criar um novo contexto por tentativa, com limite próprio, dentro do ciclo de vida do job.
Values: use pouco
context.WithValue existe, mas não deve virar um mapa global invisível. Use values para dados transversais que atravessam fronteiras de API: request_id, trace span, usuário autenticado em middleware, logger enriquecido ou metadados de observabilidade. Não use para dependências, configuração, conexão com banco ou parâmetros normais de função.
Um bom teste mental: se o dado é obrigatório para a regra de negócio, ele provavelmente deve ser argumento explícito. Se ele é infraestrutura de requisição, pode fazer sentido no contexto.
type contextKey string
const requestIDKey contextKey = "request_id"
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
func RequestID(ctx context.Context) string {
id, _ := ctx.Value(requestIDKey).(string)
return id
}
Prefira tipos de chave não exportados para evitar colisões entre pacotes. Evite chaves string genéricas como "user" ou "id".
Erros comuns com context em Go
O primeiro erro é criar context.Background() no meio da aplicação. Background é adequado no main, em testes, em inicialização ou como raiz de processo. Dentro de um handler, serviço ou repositório, criar Background corta o cancelamento da requisição.
O segundo erro é ignorar ctx.Err(). Quando uma operação falha, diferenciar context.Canceled de context.DeadlineExceeded ajuda a responder corretamente. Cancelamento por cliente desconectado não precisa virar erro 500 barulhento. Timeout de dependência pode virar 504, métrica e log de alerta.
O terceiro erro é timeout excessivamente curto. Se tudo tem 100 ms, qualquer variação normal vira erro. Se tudo tem 60 segundos, o sistema demora demais para degradar. Comece por orçamento realista, meça p95/p99 e ajuste com dados.
O quarto erro é usar context como desculpa para não fechar recursos. Mesmo com cancelamento, ainda feche resp.Body, rows.Close(), arquivos, channels de saída e timers quando aplicável.
Checklist de produção
Antes de considerar o uso de context.Context maduro em uma API Go, revise:
- Handlers usam
r.Context()e propagam para serviços. - Funções com I/O recebem
ctx context.Contextcomo primeiro argumento. - Queries usam
QueryContext,ExecContextou equivalente do driver. - HTTP clients usam
http.NewRequestWithContext. - Timeouts são definidos por operação, não copiados cegamente.
- Goroutines longas fazem
selectemctx.Done(). - Shutdown gracioso cancela o contexto raiz e aguarda encerramento.
- Logs distinguem cancelamento, deadline e erro real de dependência.
- Testes cobrem timeout e cancelamento quando o fluxo é crítico.
Esse checklist aparece muito em entrevistas backend porque mostra maturidade operacional. Saber escrever goroutine é básico; saber quando ela deve parar é produção.
Conclusão
context.Context é o fio que conecta requisição, serviço, banco, rede, worker e observabilidade em Go. Ele não substitui arquitetura, mas torna explícito o ciclo de vida das operações. Em aplicações pequenas, isso evita bugs chatos. Em produção, evita vazamento de goroutines, queries inúteis, chamadas externas penduradas e deploys que não encerram.
O próximo passo é aplicar o padrão nos pontos onde o custo de espera é maior: handlers HTTP, repositórios, clients externos e workers. Depois, combine isso com slog para logging estruturado, rate limiting em Go e métricas de latência. Para comparar como outra stack brasileira trata APIs, timeouts e cancelamento em frameworks mais opinativos, veja o guia de APIs REST com FastAPI no Python Dev Brasil.