Um timeout em dez linhas
A função do context.Context é dizer ao código quando parar. Aqui uma operação lenta recebe 50 ms e desiste quando o context manda:
A primeira chamada termina em 10 ms e devolve rows <nil>. A segunda precisaria de 200 ms, mas o context expira em 50 ms (contados a partir da criação dele), então ela devolve context deadline exceeded.
Nada é interrompido à força. O Go não tem como matar uma goroutine de fora. Um context é um sinal, e o código precisa verificá-lo: fazendo select em ctx.Done(), verificando ctx.Err() entre as etapas ou passando ctx para chamadas de biblioteca (http.NewRequestWithContext, db.QueryContext, exec.CommandContext) que verificam por você.
A interface Context
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
| Método | Devolve |
|---|---|
Done() | um channel que é fechado quando o context é cancelado ou expira (nil para um context que nunca pode ser cancelado) |
Err() | nil enquanto ativo, depois context.Canceled ou context.DeadlineExceeded |
Deadline() | o prazo e true, ou ok == false se não houver prazo |
Value(key) | o valor guardado sob key neste context ou em um ancestral, ou nil |
Contexts são imutáveis. Você nunca altera um; você deriva um filho dele com uma das funções With, e o filho acrescenta um sinal de cancelamento, um prazo ou um valor.
De onde vem um context
Toda árvore de contexts começa em uma raiz:
context.Background()para omain, oinit, os testes e a configuração de nível superior de servidores.context.TODO()quando uma função deveria receber um context, mas quem chama ainda não tem um. Ele se comporta exatamente como oBackground; o nome é uma marca para refatoração futura.
Dentro de um handler HTTP você não cria uma raiz. Você usa r.Context(), que o servidor cancela quando o cliente se desconecta ou quando o handler retorna.
WithCancel: parar sob demanda
context.WithCancel devolve um context filho e uma função cancel. Chamar cancel fecha o channel Done do filho e os channels Done de tudo o que foi derivado dele.
O envio do produtor fica em um select ao lado de ctx.Done(). É isso que permite que ele pare: um out <- i puro bloquearia para sempre assim que o consumidor parasse de ler, e a goroutine vazaria. O for range nums final espera até o produtor fechar o channel. Enquanto ele esvazia, o produtor ainda pode conseguir enviar um ou dois valores, porque quando os dois cases do select estão prontos o Go escolhe um ao acaso; o cancelamento é rápido, não instantâneo.
cancel pode ser chamada mais de uma vez e de qualquer goroutine. Só a primeira chamada faz algo.
WithTimeout e WithDeadline
WithTimeout(parent, d) é WithDeadline(parent, time.Now().Add(d)). Use um timeout para "no máximo este tempo" e um deadline quando você tem um horário absoluto.
Quando o tempo passa, Done fecha e Err devolve context.DeadlineExceeded. Se cancel for chamada antes, Err devolve context.Canceled. Verifique qual dos dois com errors.Is, porque as bibliotecas costumam empacotar o erro:
Sempre chame cancel, mesmo para um timeout que vai disparar sozinho. O context segura um timer e uma posição no pai até que uma dessas coisas aconteça, e defer cancel() libera os dois assim que a função retorna. O go vet reporta uma função cancel descartada: the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak.
Filhos não sobrevivem aos pais
Contexts formam uma árvore. Cancelar um pai cancela todos os descendentes. Um filho pode ter um prazo mais curto que o do pai, nunca mais longo: o prazo mais cedo sempre vence.
É isso que torna os contexts úteis entre camadas. Um handler HTTP recebe um context que morre junto com a requisição; uma chamada de banco de dados três camadas abaixo deriva dele um timeout de 2 segundos. Se o cliente desligar depois de 100 ms, a consulta é cancelada nesse momento, não dois segundos depois.
Sempre faça select em ctx.Done() quando bloquear
Qualquer goroutine que espera (em um envio de channel, um recebimento, um timer) deve esperar em ctx.Done() ao mesmo tempo. Para laços que só usam CPU e nunca bloqueiam, verifique ctx.Err() de tempos em tempos:
for i, item := range items {
if i%1000 == 0 {
if err := ctx.Err(); err != nil {
return err
}
}
process(item)
}
Use time.After dentro de um select para uma espera simples, mas prefira um timer que você pode parar (ou um timeout de context) quando a espera puder ser cancelada com frequência.
Causas de cancelamento (Go 1.20 e 1.21)
ctx.Err() só diz canceled ou deadline exceeded. Para registrar o porquê, use as variantes Cause:
WithCancelCause chegou no Go 1.20, e WithTimeoutCause e WithDeadlineCause no Go 1.21. Err continua devolvendo os valores padrão para que as verificações existentes continuem funcionando; context.Cause dá o detalhe.
WithValue, com moderação
context.WithValue(parent, key, value) anexa um valor. ctx.Value(key) o procura pela cadeia de pais.
Regras para valores:
- Use um tipo não exportado para as chaves, nunca uma
stringsimples. Dois pacotes que usam"user"sobrescreveriam um ao outro. (Ogo vetnão pega isso; ostaticcheckpega.) - Envolva o acesso em funções auxiliares tipadas como
WithRequestIDeRequestID, para que quem chama nunca vejaanynem a chave. - Guarde só dados do escopo da requisição que atravessam APIs: IDs de trace e de requisição, o usuário autenticado, um logger. Nunca parâmetros opcionais, conexões de banco de dados ou configuração. Esses pertencem aos argumentos de função ou a campos de struct, onde o compilador consegue verificá-los e quem lê consegue vê-los.
- A busca percorre a cadeia um pai de cada vez, então cada valor que você acrescenta deixa a busca pelos outros um passo mais longa.
Convenções
ctx context.Contexté o primeiro parâmetro de qualquer função que faz I/O, bloqueia ou chama algo que faz isso:func Fetch(ctx context.Context, url string) error.- Não guarde um context em uma struct. Passe-o em cada chamada de método. Um context pertence a uma operação, e uma struct normalmente vive mais que ela. (A exceção é um tipo que representa uma única operação, como
http.Request.) - Nunca passe
nilcomo context. Usecontext.TODO()se não tiver nada melhor. - Devolva
ctx.Err(), ou empacote-o com%w, quando você parar por causa do context, para que quem chama consiga diferenciar um timeout de uma falha real.
Context em servidores e clientes HTTP
Do lado do servidor: r.Context() é cancelado quando o cliente se desconecta, quando o handler retorna ou quando um stream HTTP/2 é resetado. Do lado do cliente: http.NewRequestWithContext faz a requisição respeitar um timeout ou um cancelamento. Este programa executa as duas pontas por meio do httptest:
O cliente desiste em 50 ms e fecha a conexão. O servidor percebe, o context da requisição é cancelado, e o handler para em vez de gastar mais 450 milissegundos em um relatório que ninguém vai ler. Em um handler real, você passa r.Context() para todas as chamadas de banco de dados e HTTP, e todas param juntas.
Outras funções auxiliares (Go 1.21)
context.WithoutCancel(ctx)devolve um context com os mesmos valores, mas que não é cancelado quandoctxé. Use-o para trabalho que precisa terminar depois do fim da requisição, como gravar um log de auditoria.context.AfterFunc(ctx, f)executafna sua própria goroutine quandoctxtermina, e devolve uma funçãostoppara cancelar o registro.
Erros comuns
- Não chamar
cancel. Sempre usedefer cancel()logo depois deWithCancel,WithTimeoutouWithDeadline. - Iniciar uma goroutine que ignora o
ctx. Se ela bloqueia sem fazer select emctx.Done(), cancelar não faz nada e a goroutine vaza. - Criar um
context.Background()novo no fundo de uma cadeia de chamadas. Isso corta a ligação com o prazo e o cancelamento de quem chamou. Passe octxque você recebeu. - Comparar erros com
==. Useerrors.Is(err, context.DeadlineExceeded); a maioria das bibliotecas o empacota. - Usar
WithValuepara dependências. Uma conexão de banco escondida em um context é um parâmetro que o compilador não consegue mais verificar. - Esperar que o cancelamento seja instantâneo. O código só percebe na próxima verificação. Um laço longo sem verificação continua executando.
Perguntas frequentes
Para que serve o context em Go?
Um context.Context diz a uma função, e a tudo o que ela chama, quando desistir: porque quem chamou cancelou, porque um prazo passou ou porque o cliente se desconectou. Ele também pode carregar valores do escopo da requisição, como um ID de requisição. Por convenção, é o primeiro parâmetro e se chama ctx.
Qual a diferença entre context.Background e context.TODO?
Os dois devolvem um context vazio, que nunca é cancelado e não tem prazo nem valores. Eles se comportam de forma idêntica. Background() é a raiz para o main, os testes e a configuração de nível superior. TODO() marca um lugar onde um context de verdade deveria ser passado, mas o código ao redor ainda não tem um, o que facilita encontrá-lo depois.
Por que preciso chamar cancel depois de context.WithTimeout?
WithTimeout, WithDeadline e WithCancel registram o novo context no pai e podem iniciar um timer. Chamar cancel libera esses recursos assim que você termina, em vez de quando o timeout dispara ou o pai é cancelado. Escreva defer cancel() logo depois de criá-lo; o go vet avisa quando uma função cancel é descartada.
O que significa "context deadline exceeded" em Go?
É o texto de context.DeadlineExceeded, o erro que ctx.Err() devolve quando o prazo de um context passou. Funções que respeitam o context, como clientes HTTP e drivers de banco de dados, o devolvem (muitas vezes empacotado) quando o tempo acaba. Verifique com errors.Is(err, context.DeadlineExceeded).
Devo usar context.WithValue para passar parâmetros?
Não. Use-o só para dados do escopo da requisição que atravessam fronteiras de API e que as funções no meio do caminho não precisam conhecer, como um trace ID ou o usuário autenticado. Tudo o que uma função precisa para fazer o seu trabalho pertence aos parâmetros dela, onde o compilador verifica.