O pacote context é uma das peças mais importantes do ecossistema Go. Ele resolve um problema fundamental em sistemas concorrentes: como sinalizar cancelamento, deadlines e passar metadados entre goroutines de forma segura e padronizada. Se você escreve APIs, acessa bancos de dados ou trabalha com concorrência em Go, dominar context.Context é essencial.
O que é Context?
context.Context é uma interface que carrega deadlines, sinais de cancelamento e valores request-scoped através das fronteiras de uma API. Toda chamada que pode demorar ou ser cancelada deve receber um context.Context como primeiro parâmetro.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
Deadline()retorna quando o context vai expirarDone()retorna um canal que é fechado quando o context é canceladoErr()retorna o motivo do cancelamentoValue()retorna valores associados ao context
Context Raiz: Background vs TODO
Todo context começa com uma raiz. Go oferece dois:
// Para produção: o context raiz padrão
ctx := context.Background()
// Para código em desenvolvimento onde o context ainda não foi definido
ctx := context.TODO()
context.Background() é o que você usa em 99% dos casos — no main(), na inicialização de serviços e como raiz de novos contexts. Use context.TODO() apenas como marcador temporário enquanto está refatorando código que ainda não recebe context.
WithCancel: Cancelamento Manual
WithCancel cria um context filho que pode ser cancelado manualmente. Quando o context pai é cancelado, os filhos também são.
func processarDados(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel() // SEMPRE chame cancel para liberar recursos
resultados := make(chan string, 3)
// Lançar 3 goroutines para buscar dados
for i := 0; i < 3; i++ {
go func(id int) {
// Simular busca de dados
time.Sleep(time.Duration(id) * time.Second)
select {
case resultados <- fmt.Sprintf("resultado-%d", id):
case <-ctx.Done():
return // Context cancelado, encerrar goroutine
}
}(i)
}
// Pegar apenas o primeiro resultado e cancelar o resto
select {
case r := <-resultados:
fmt.Println("Recebido:", r)
cancel() // Cancela as outras goroutines
return nil
case <-ctx.Done():
return ctx.Err()
}
}
A regra de ouro: sempre chame cancel(), mesmo que o context já tenha expirado. O defer cancel() logo após a criação garante que os recursos sejam liberados.
WithTimeout e WithDeadline
WithTimeout e WithDeadline criam contexts que expiram automaticamente. A diferença é sutil: WithTimeout recebe uma duração, WithDeadline recebe um ponto no tempo.
func buscarUsuario(ctx context.Context, id string) (*Usuario, error) {
// Timeout de 3 segundos para a operação inteira
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET",
fmt.Sprintf("https://api.exemplo.com/usuarios/%s", 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 usuario %s", 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
}
Para WithDeadline, o uso é similar mas com um horário absoluto:
// A operação deve terminar até às 23:59:59 de hoje
deadline := time.Now().Truncate(24 * time.Hour).Add(24*time.Hour - time.Second)
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
WithValue: Passando Valores
WithValue permite associar valores ao context. Use com moderação — context não é um substituto para parâmetros de função.
// Defina tipos privados para chaves — evita colisão
type chaveContexto string
const (
chaveRequestID chaveContexto = "request_id"
chaveUserID chaveContexto = "user_id"
)
func middlewareRequestID(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)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func obterRequestID(ctx context.Context) string {
if id, ok := ctx.Value(chaveRequestID).(string); ok {
return id
}
return "desconhecido"
}
Quando usar WithValue: request ID, user ID de autenticação, trace ID para observabilidade. Quando NÃO usar: parâmetros de negócio, configurações, dependências. Esses devem ser parâmetros explícitos da função.
Context em HTTP Handlers
Em servidores HTTP, cada request já vem com um context que é cancelado quando o cliente desconecta:
func handlerBusca(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
termo := r.URL.Query().Get("q")
// Adicionar timeout à busca
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
resultados, err := buscarNoIndice(ctx, termo)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
http.Error(w, "Busca demorou demais", http.StatusGatewayTimeout)
return
}
if ctx.Err() == context.Canceled {
// Cliente desconectou — não precisa responder
return
}
http.Error(w, "Erro interno", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(resultados)
}
Quando o cliente fecha a conexão, r.Context() é cancelado automaticamente. Isso permite que suas goroutines downstream parem de trabalhar em uma resposta que ninguém vai receber. Para mais detalhes sobre APIs em Go, veja o tutorial de API REST com Go.
Context com Banco de Dados
Todas as operações de banco em Go aceitam context — use-os para evitar queries que travam:
func listarPedidos(ctx context.Context, db *sql.DB, userID int) ([]Pedido, error) {
// Timeout de 10 segundos para a query
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx,
"SELECT id, produto, valor FROM pedidos WHERE user_id = $1 ORDER BY created_at DESC",
userID,
)
if err != nil {
return nil, fmt.Errorf("erro ao consultar pedidos: %w", err)
}
defer rows.Close()
var pedidos []Pedido
for rows.Next() {
var p Pedido
if err := rows.Scan(&p.ID, &p.Produto, &p.Valor); err != nil {
return nil, fmt.Errorf("erro ao ler pedido: %w", err)
}
pedidos = append(pedidos, p)
}
return pedidos, rows.Err()
}
Se o context for cancelado enquanto a query está rodando, o driver do banco cancela a operação no servidor. Isso é crucial para manter a saúde do PostgreSQL e evitar conexões presas.
Boas Práticas
Siga estas regras para usar context de forma idiomática:
Context é sempre o primeiro parâmetro, nomeado
ctx:// Correto func Processar(ctx context.Context, dados []byte) error // Errado func Processar(dados []byte, ctx context.Context) errorNunca armazene context em uma struct. Passe-o explicitamente em cada chamada de método.
Sempre chame cancel() — use
defer cancel()imediatamente após criar o context.Não passe context nil. Se não sabe qual context usar, use
context.TODO().WithValue é para metadados request-scoped, não para parâmetros de função.
Verifique
ctx.Done()em loops longos para permitir cancelamento:for _, item := range itens { select { case <-ctx.Done(): return ctx.Err() default: } processar(ctx, item) }
Anti-Patterns Comuns
Ignorar o context recebido
// ERRADO: cria um context novo, ignorando timeouts do chamador
func BuscarDados(ctx context.Context) error {
ctx = context.Background() // NÃO faça isso!
// ...
}
Usar strings como chaves de WithValue
// ERRADO: colisão de chaves entre pacotes
ctx = context.WithValue(ctx, "user_id", 123)
// CORRETO: tipo privado para a chave
type chaveUserID struct{}
ctx = context.WithValue(ctx, chaveUserID{}, 123)
Esquecer de verificar o context em operações longas
Se sua função executa um loop ou múltiplas operações IO, verifique ctx.Done() periodicamente. Caso contrário, o cancelamento só será percebido na próxima chamada que aceita context. Para mais sobre tratamento de erros em Go, confira nosso guia dedicado.
Próximos Passos
Para aprofundar seu conhecimento sobre concorrência e operações assíncronas em Go:
- Concorrência em Go — goroutines, channels e patterns
- API REST com Go — aplicação prática de context em APIs
- PostgreSQL com Go — context em operações de banco
- Tratamento de Erros — como combinar context com error handling
- Para ver como outras linguagens abordam concorrência, compare com Python asyncio ou Kotlin coroutines
Dominar context.Context é fundamental para escrever código Go robusto e production-ready. Comece aplicando timeouts nas chamadas externas e propague o context por toda a cadeia de chamadas — seu sistema vai agradecer quando uma dependência falhar.