Context no Go: timeout, cancel e passagem de valores
1. Visão geral
Ao escrever um serviço Go que lida com muitas goroutines, você precisa de uma forma unificada de dizer à goroutine filha: “pare, não preciso mais do resultado” ou “execute por apenas 2 segundos e depois desista”. context.Context foi criado para resolver exatamente esse problema.
Este artigo passa por três coisas principais que você faz com context: cancelamento (cancel) manual, definir timeout/deadline e passar valores request-scoped através das camadas de chamadas de funções. Após ler, você será capaz de escrever handlers HTTP, funções que chamam DB e funções que chamam goroutines filhas que sabem se parar no momento certo, sem deixar goroutines vazarem.
2. O que é Context e por que você precisa dele
context.Context é uma interface que descreve o “contexto” de uma thread de processamento (geralmente uma requisição). Ele carrega consigo:
- Sinal de cancelamento (cancel signal).
- Deadline/timeout.
- Alguns valores pequenos de chave-valor, anexados à requisição.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
Done()retorna um channel que é fechado quando o context é cancelado ou expira.Err()diz o motivo:context.Canceledoucontext.DeadlineExceeded.Value()obtém o valor anexado ao context.
Nota: Context não é usado para passar parâmetros opcionais arbitrários. Ele deve apenas carregar sinais de controle e dados request-scoped como ID de requisição, usuário atual — não config ou objetos de negócio importantes.
3. Cancelamento manual com WithCancel
Use quando você quer decidir quando cancelar, por exemplo, quando uma das muitas goroutines tem um resultado, cancele as goroutines restantes.
func fetchFromMultipleSources(ctx context.Context) (string, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel() // sempre chame cancel para liberar recursos
resultCh := make(chan string, 1)
for _, src := range []string{"cache", "db", "api"} {
go func(source string) {
select {
case <-time.After(100 * time.Millisecond):
resultCh <- source
cancel() // tem resultado, cancele as outras fontes
case <-ctx.Done():
return // foi cancelado, saia cedo
}
}(src)
}
select {
case res := <-resultCh:
return res, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
context.WithCancel(ctx)cria um context filho e uma função cancel para cancelá-lo.defer cancel()é obrigatório: se você não chamar, o context filho fica “pendurado” até que o pai seja cancelado, causando vazamento de goroutine esperando emctx.Done().- Qualquer goroutine que termine primeiro chama
cancel(), as goroutines restantes recebem o sinal viactx.Done()e param.
Erro comum: esquecer de defer cancel(). Se a função retornar cedo (por exemplo, por erro) sem cancelar o context, a goroutine filha ainda vive até ser coletada pelo garbage collector indiretamente — na verdade, é um vazamento.
4. Timeout e deadline com WithTimeout, WithDeadline
4.1. WithTimeout para limite de tempo relativo
func callPaymentService(ctx context.Context, orderID string) error {
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
"https://payment.local/orders/"+orderID, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err // pode ser context.DeadlineExceeded
}
defer resp.Body.Close()
return nil
}
context.WithTimeout(ctx, 2*time.Second)cancela automaticamente o context após 2 segundos.http.NewRequestWithContextanexa o context à requisição; quando o context expira,http.Clientcancela automaticamente a requisição em andamento.- Se passar de 2 segundos,
resp, err := http.DefaultClient.Do(req)retorna err contendocontext.DeadlineExceeded.
Verificando o erro de timeout:
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.Println("serviço de pagamento expirou")
}
return err
}
4.2. WithDeadline para ponto de tempo absoluto
func processBeforeMidnight(ctx context.Context) error {
deadline := time.Date(2026, 10, 6, 23, 59, 59, 0, time.Local)
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
select {
case <-time.After(5 * time.Hour):
return errors.New("processamento concluído")
case <-ctx.Done():
return ctx.Err()
}
}
WithDeadlineé usado quando você tem um ponto de tempo específico (por exemplo, fim do dia), diferente deWithTimeoutque é um intervalo de tempo relativo a partir de agora.- Essencialmente,
WithTimeout(ctx, d)é apenas um wrapper fino que chamaWithDeadline(ctx, time.Now().Add(d)).
5. Passando valores através de context com WithValue
Use para dados anexados à requisição, não para parâmetros de negócio.
type requestIDKey struct{}
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey{}, id)
}
func RequestIDFromContext(ctx context.Context) (string, bool) {
id, ok := ctx.Value(requestIDKey{}).(string)
return id, ok
}
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
ctx := WithRequestID(r.Context(), id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func orderHandler(w http.ResponseWriter, r *http.Request) {
if id, ok := RequestIDFromContext(r.Context()); ok {
log.Printf("[%s] processando pedido", id)
}
}
- Use
type requestIDKey struct{}como chave separada — evite usar string diretamente pois é fácil colidir com chave de outro package. WithRequestIDeRequestIDFromContextencapsulam a operação set/get, outros packages não precisam saber qual é a chave real.r.WithContext(ctx)cria uma nova requisição carregando o context com valor anexado, passando adiante através da cadeia de middleware.
Nota: Não use
context.WithValuepara passar*sql.DB, config ou qualquer coisa que a função precisa para funcionar corretamente. Essas coisas devem ser parâmetros explícitos ou campos de struct. Context value é apropriado apenas para dados “nice to have” como ID de requisição, ID de rastreamento, usuário autenticado.
6. context.TODO e context.Background
func main() {
ctx := context.Background() // context raiz, nunca é cancelado
run(ctx)
}
func legacyFunctionBeingRefactored() {
ctx := context.TODO() // placeholder, será substituído pelo context real depois
_ = ctx
}
context.Background()é o context raiz, usado em main, testes ou em inicialização top-level.context.TODO()é usado quando você está refatorando e não tem certeza qual context deve passar — funcionalmente igual aBackground(), mas sinaliza claramente a intenção para quem lê o código e para ferramentas linter.
7. Propagando context através de múltiplas camadas de chamadas de funções
Convenção: ctx context.Context é sempre o primeiro parâmetro.
func (s *OrderService) CreateOrder(ctx context.Context, order Order) error {
if err := s.validate(ctx, order); err != nil {
return err
}
return s.repo.Save(ctx, order)
}
func (r *OrderRepository) Save(ctx context.Context, order Order) error {
_, err := r.db.ExecContext(ctx,
"INSERT INTO orders (id, total) VALUES (?, ?)", order.ID, order.Total)
return err
}
ExecContextaceita context, se a requisição HTTP acima for cancelada (cliente fecha a conexão), o driver DB cancela imediatamente a query em execução — evita manter a conexão desnecessariamente.- Regra: qualquer função que chame I/O (HTTP, DB, gRPC) deve aceitar context e passá-lo para as APIs que suportam context (sufixo
*Context).
8. Testando o comportamento com teste
func TestFetchFromMultipleSources_ReturnsFastestSource(t *testing.T) {
ctx := context.Background()
result, err := fetchFromMultipleSources(ctx)
require.NoError(t, err)
assert.Contains(t, []string{"cache", "db", "api"}, result)
}
func TestCallPaymentService_TimesOutWhenSlow(t *testing.T) {
slowServer := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
time.Sleep(3 * time.Second)
}))
defer slowServer.Close()
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, slowServer.URL, nil)
_, err := http.DefaultClient.Do(req)
assert.ErrorIs(t, err, context.DeadlineExceeded)
}
<!-- go.mod: adicione a biblioteca de assertion (não é XML, apenas uma nota) -->
go get github.com/stretchr/testify
- O primeiro teste usa
context.Background()porque não é necessário controlar timeout em um unit test normal. - O teste seguinte define propositalmente um timeout de 500ms menor que o tempo de processamento do servidor (3s) para confirmar que o erro retornado é realmente
context.DeadlineExceeded. httptest.NewServerconstrói um servidor falso, sem necessidade de mock complexo.
9. Erros comuns
- Não chamar cancel(): sempre
defer cancel()logo após chamarWithCancel/WithTimeout/WithDeadline, mesmo que o context expire sozinho — Go vet avisará se você esquecer. - Armazenar context em campo de struct: context deve ser passado como parâmetro de função, não armazenado como campo de struct (exceto em alguns casos especiais como
http.Request). Armazenar em campo facilmente leva a context antigo, stale. - Usar context para passar parâmetro obrigatório: se a função não funciona corretamente sem esse valor, passe-o como um parâmetro normal, não o esconda em context.
- Criar novo context ao invés de usar context pai: se você chamar
context.Background()dentro de uma função que está recebendoctxdo chamador, você perde a capacidade de cancelamento/timeout de cima — sempre derive do context recebido:context.WithTimeout(ctx, ...), nãocontext.WithTimeout(context.Background(), ...).
Quando você não precisa de context: com funções puras (pure function), sem I/O, sem goroutine, você não precisa de context como parâmetro — adicioná-lo só causa confusão.
10. Conclusão
Context resolve três coisas centrais na concorrência Go: sinalizar cancelamento, aplicar limites de tempo e levar alguns valores pequenos através das camadas de chamadas de funções. A regra mais segura é sempre aceitar ctx como primeiro parâmetro, sempre fazer defer cancel() e usar WithValue apenas para dados request-scoped que são realmente auxiliares. Dominando esses princípios, você será capaz de escrever serviços Go sem vazamento de goroutine e responder adequadamente quando o cliente faz timeout ou cancela a requisição.