O que é context.Context e por que ele existe
Em operações concorrentes e/ou que dependem de I/O (rede, banco, filas, chamadas a serviços), é comum precisar: (1) cancelar um conjunto de tarefas quando o usuário desistiu, quando uma requisição expirou ou quando um erro crítico aconteceu; (2) impor prazos (timeout/deadline) para evitar goroutines “presas” indefinidamente; (3) propagar sinais de cancelamento e metadados de requisição por camadas (handlers, serviços, repositórios, clientes HTTP etc.).
Em Go, o padrão idiomático para isso é o pacote context. Um context.Context é um valor imutável que carrega: um canal de cancelamento (Done()), um erro de término (Err()), um prazo (Deadline()) e, opcionalmente, valores (Value()) para dados de escopo de requisição.
Regras práticas (as que mais evitam bugs)
- O
context.Contextdeve ser o primeiro parâmetro de funções/métodos que podem bloquear, fazer I/O ou iniciar trabalho concorrente:func (s *Service) Do(ctx context.Context, ...). - Não armazene
context.Contextem structs para “reusar depois”. Contexto é específico de uma operação; guardar em struct costuma vazar cancelamentos, misturar requisições e causar data races. - Não passe
nilcomo contexto. Usecontext.Background()(oucontext.TODO()quando ainda não sabe qual usar). - Sempre chame a função de cancelamento retornada por
WithCancel/WithTimeout/WithDeadlinepara liberar recursos internos (timers, referências). - Use
context.WithValuecom parcimônia: apenas para metadados de requisição (ex.: request-id), nunca para dependências (DB, logger, configs).
Contextos base: Background e TODO
context.Background() é o contexto raiz típico em programas (ex.: em main) e em testes. Ele nunca é cancelado e não tem deadline.
ctx := context.Background()context.TODO() é útil quando você ainda não tem um contexto “real” para propagar, mas quer manter a assinatura pronta. Em produção, prefira Background ou o contexto recebido de uma camada superior.
Cancelamento manual com context.WithCancel
WithCancel cria um contexto filho que pode ser cancelado explicitamente. Cancelar o pai cancela o filho; cancelar o filho não cancela o pai.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Passo a passo
- Crie um contexto cancelável:
ctx, cancel := context.WithCancel(parent). - Inicie goroutines que observam
ctx.Done(). - Quando detectar uma condição de parada (erro crítico, usuário cancelou, etc.), chame
cancel(). - Garanta
defer cancel()para liberar recursos mesmo se tudo terminar “normalmente”.
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(200 * time.Millisecond):
fmt.Println("worker", id, "tick")
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(700 * time.Millisecond)
cancel() // cancela todos que dependem de ctx
}()
err := worker(ctx, 1)
fmt.Println("worker terminou:", err)
}Note como o select permite que a goroutine acorde tanto pelo “trabalho” quanto pelo cancelamento. Sem isso, ela poderia ficar bloqueada e ignorar o cancelamento por muito tempo.
Timeout com context.WithTimeout
WithTimeout é um atalho para criar um contexto com deadline relativo ao momento atual. Quando o tempo expira, o contexto é cancelado automaticamente com erro context.DeadlineExceeded.
Passo a passo
- Crie:
ctx, cancel := context.WithTimeout(parent, 2*time.Second). defer cancel()sempre.- Em loops e operações bloqueantes, observe
ctx.Done().
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case <-time.After(5 * time.Second):
// simula operação lenta
fmt.Println("terminou")
case <-ctx.Done():
fmt.Println("cancelado por timeout:", ctx.Err()) // DeadlineExceeded
}Deadline absoluto com context.WithDeadline
WithDeadline define um instante exato no tempo. É útil quando você já tem um prazo absoluto (por exemplo, um SLA calculado, ou um deadline recebido de outra camada).
deadline := time.Now().Add(1500 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()Na prática, WithTimeout e WithDeadline são equivalentes em efeito; a diferença é como você expressa o prazo.
Como propagar contexto por camadas (sem guardar em struct)
O contexto deve fluir “de cima para baixo” na chamada: quem inicia a operação cria/recebe o contexto e passa adiante. Cada camada pode derivar um contexto filho (por exemplo, com timeout menor) e repassar.
Exemplo de design
type Service struct {
repo *Repo
}
type Repo struct {
// dependências aqui (db, client, etc.), mas NÃO context
}
func (s *Service) Process(ctx context.Context, userID string) error {
// camada de serviço decide um timeout específico para esta etapa
ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)
defer cancel()
return s.repo.LoadUser(ctx, userID)
}
func (r *Repo) LoadUser(ctx context.Context, userID string) error {
// aqui você usaria ctx em chamadas de I/O (ex.: QueryContext)
select {
case <-time.After(300 * time.Millisecond):
return nil
case <-ctx.Done():
return ctx.Err()
}
}Anti-padrão: armazenar contexto em struct
Evite algo como:
type Service struct {
ctx context.Context // NÃO faça isso
}Isso tende a causar: cancelamento de uma operação afetando outra, dificuldade de testes, e confusão sobre qual contexto está ativo.
Integração com goroutines: cancelamento cooperativo com select
Cancelamento em Go é cooperativo: a goroutine precisa checar o contexto e sair. O padrão mais comum é usar select com ctx.Done() em pontos onde a goroutine poderia bloquear ou em loops.
Padrão: loop com trabalho periódico
func poll(ctx context.Context, out chan<- int) error {
defer close(out)
i := 0
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(100 * time.Millisecond):
i++
out <- i
}
}
}Padrão: esperar múltiplos resultados e cancelar o restante ao primeiro erro
Quando você dispara várias tarefas concorrentes, um erro crítico pode tornar inútil continuar. Nesse caso, cancele o contexto para pedir que as outras parem.
type result struct {
id int
err error
}
func task(ctx context.Context, id int) error {
// simula trabalho variável
select {
case <-time.After(time.Duration(200+id*120) * time.Millisecond):
if id == 2 {
return fmt.Errorf("erro crítico na tarefa %d", id)
}
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func runAll(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
resCh := make(chan result, 3)
for i := 1; i <= 3; i++ {
i := i
go func() {
err := task(ctx, i)
resCh <- result{id: i, err: err}
}()
}
for i := 0; i < 3; i++ {
res := <-resCh
if res.err != nil {
cancel() // pede para as outras pararem
return res.err
}
}
return nil
}Detalhe importante: o canal resCh é bufferizado para evitar que goroutines fiquem bloqueadas tentando enviar resultado após o retorno antecipado. Alternativamente, você poderia drenar o canal antes de retornar, ou usar errgroup (pacote golang.org/x/sync/errgroup) em projetos reais.
Interpretando ctx.Err() corretamente
Quando <-ctx.Done() dispara, ctx.Err() explica o motivo:
context.Canceled: cancelamento manual (ou cancelamento do pai).context.DeadlineExceeded: deadline/timeout atingido.
Em funções internas, normalmente você retorna ctx.Err() diretamente. Em camadas superiores, você pode decidir se isso é erro “esperado” (ex.: timeout do usuário) ou se deve ser logado/tratado como falha.
Exercício prático: pipeline concorrente com timeout e erro crítico
Objetivo: implementar um coordenador que executa N tarefas concorrentes. O sistema deve parar corretamente quando: (1) o timeout global expirar; ou (2) qualquer tarefa retornar um erro crítico. As tarefas devem observar o contexto e encerrar rapidamente.
Especificação
- Crie uma função
RunTasks(ctx context.Context, n int) error. - Dentro dela, derive um contexto com timeout global de
1*time.Secondusandocontext.WithTimeout. - Inicie
ngoroutines, cada uma executandoDoWork(ctx, id). DoWorkdeve simular trabalho em etapas (ex.: 10 iterações), e em cada iteração usarselectpara respeitarctx.Done().- Faça uma das tarefas (por exemplo,
id == 3) retornar um erro crítico em alguma etapa (ex.: na iteração 4). - Ao receber o primeiro erro crítico, cancele o contexto (via
WithCancelou reutilizando o cancel do timeout) e retorne esse erro. - Se o timeout expirar antes de todas terminarem, retorne
ctx.Err()(que seráDeadlineExceeded). - Garanta que não haja goroutines bloqueadas ao tentar enviar resultados (use canal bufferizado ou drenagem).
Esqueleto sugerido
package main
import (
"context"
"errors"
"fmt"
"time"
)
type taskResult struct {
id int
err error
}
var ErrCritical = errors.New("erro crítico")
func DoWork(ctx context.Context, id int) error {
for step := 1; step <= 10; step++ {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(120 * time.Millisecond):
// simulando uma etapa de trabalho
if id == 3 && step == 4 {
return fmt.Errorf("tarefa %d: %w", id, ErrCritical)
}
}
}
return nil
}
func RunTasks(ctx context.Context, n int) error {
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
resCh := make(chan taskResult, n)
for id := 1; id <= n; id++ {
id := id
go func() {
err := DoWork(ctx, id)
resCh <- taskResult{id: id, err: err}
}()
}
for i := 0; i < n; i++ {
select {
case <-ctx.Done():
return ctx.Err()
case res := <-resCh:
if res.err != nil {
// se for crítico, cancele tudo
if errors.Is(res.err, ErrCritical) {
cancel()
return res.err
}
// se não for crítico, você decide: acumular e continuar ou retornar
return res.err
}
}
}
return nil
}
func main() {
err := RunTasks(context.Background(), 5)
fmt.Println("resultado:", err)
}Checklist de validação do exercício
- Ao atingir 1 segundo, o programa retorna com
context.DeadlineExceedede as goroutines param (não continuam imprimindo/trabalhando). - Quando a tarefa 3 falha com
ErrCritical, as demais encerram rapidamente comcontext.Canceled(ou tambémDeadlineExceededse o tempo já tiver estourado). - Não há deadlock ao retornar cedo (canal bufferizado ou outra estratégia equivalente).
- As assinaturas recebem
ctxcomo primeiro parâmetro e ele é propagado por chamadas internas.