Introdução
Uma das responsabilidades que possuo onde trabalho atualmente é ensinar e orientar pessoas menos experientes, e uma das principais dúvidas que recebo relacionadas a Go é sobre os contextos.
CitaçãoMas eu não aguento mais passar isso em praticamente toda chamada que faço. Outras linguagens não têm isso, por que eu tenho que me preocupar?
- Pessoa desenvolvedora vendo contextos em todo lugar
Para entender a utilidade dos contextos, precisamos estar cientes de que concorrência é algo comum no desenvolvimento atual, mesmo que não percebamos.
Novas tarefas podem surgir das mais diversas formas: processos, threads, goroutines (ou green threads). Além de surgir, elas também podem — e serão — interrompidas.
O Go nos oferece uma forma de sinalizar que um conjunto de tarefas precisa ser cancelado e também de tratar esse sinal no código de um sistema.
Interrupções no dia a dia
Nosso cotidiano é repleto de situações que não saem como o planejado. Muitas vezes, precisamos ter um plano B ou parar para pensar em como lidar com essas mudanças. Nos sistemas que construímos, isso não é diferente — exceto que os computadores fazem exatamente o que pedimos, sem a capacidade de se adaptar sozinhos.
Para ilustrar essa ideia, vamos a uma analogia:
AnalogiaImagine que você vai almoçar em um restaurante. Ao se sentar, escolhe um prato do cardápio, chama o garçom e faz seu pedido.
O garçom anota o pedido e o envia para a cozinha, onde os cozinheiros começam a prepará-lo.
Agora, imagine que, no meio disso, você recebe uma ligação urgente e precisa sair imediatamente. Você cancela o pedido com o garçom.
O que acontece com o prato que estava sendo preparado? Isso depende do funcionamento do restaurante: ele pode descartar tudo o que já foi feito, ou tentar reaproveitar os ingredientes que ainda estão bons.
Assim como o restaurante precisou lidar com o pedido cancelado, em sistemas concorrentes também há momentos em que precisamos interromper uma tarefa em andamento.
No exemplo, você percebeu que precisava sair e não voltaria, então cancelou o pedido com o garçom — que, por sua vez, comunicou o cancelamento à cozinha.
O que isso tem a ver com meu código?
Além do exemplo do restaurante, podemos pensar em situações mais próximas da realidade de quem desenvolve: pode ser o download de um arquivo grande, uma requisição para uma API REST, uma consulta ao banco de dados ou a execução de qualquer tarefa que pode levar muito tempo para completar.
Como você faria para cancelar algo, se fosse necessário? E como notificaria esse cancelamento em efeito cascata para todas as operações relacionadas?
Às vezes, uma simples sinalização é suficiente. Em outros casos, como no restaurante, o cancelamento precisa ser propagado por diferentes partes do sistema — e pode ter consequências importantes que precisam ser tratadas adequadamente.
Cada linguagem, biblioteca ou framework oferece sua própria solução para lidar
tanto com o ciclo de vida quanto com o tempo de vida de fluxos e operações. Os
desenvolvedores do Go enxergaram essa necessidade — e é aí que entra o pacote
context
.
Mas antes de falar sobre contextos, vale lembrar: o Go foi criado com a filosofia de que ser explícito é melhor do que esconder complexidade por trás de abstrações implícitas.
CitaçãoExplícito é melhor que implícito
- Item 2 do Zen of Python
Apesar das coisas nem sempre serem tão explícitas assim no Python, esse é um bom conselho a ser seguido para qualquer linguagem. Inclusive, recomendo também a leitura do Zen of Go.
Entrando no contexto
Agora que entendemos por que é importante poder interromper tarefas, vamos ver
como o Go nos ajuda a fazer isso de forma estruturada com o pacote context
.
O pacote context
define um padrão para lidar com cenários em que sinais de
cancelamento, timeouts ou deadlines precisam ser propagados ao longo da
execução. Ele permite que fluxos sejam encerrados de forma controlada — o que
costumamos chamar de graceful shutdown.
A interface context.Context
Vamos começar olhando a documentação da interface context.Context
. Na descrição do tipo, vemos:
CitaçãoA Context carries a deadline, a cancellation signal, and other values across API boundaries.
Context’s methods may be called by multiple goroutines simultaneously.
TraduçãoUm contexto carrega um prazo, um sinal de cancelamento, e outros valores através dos limites da API.
Os métodos do tipo Context podem ser chamados simultaneamente por múltiplas goroutines
Como cancelar contextos?
Se você foi curioso ou curiosa e também deu uma olhada na própria definição da
interface, possivelmente notou que não existe um método Cancel()
ou algo
parecido. Ou seja, um contexto não tem capacidade de ativar um sinal de
cancelamento para ele mesmo.
Por que os contextos não podem cancelar a si próprios?
Para explicar o motivo, vamos voltar ao exemplo do restaurante: imagine que o garçom ou outro cliente pudesse cancelar seu pedido, ou pior, todos os pedidos. Você também pode pensar em uma tarefa que inicia várias goroutines em paralelo para realizar o download de vários arquivos — a exposição de uma forma de cancelar dentro do próprio contexto poderia possibilitar o cancelamento de outros downloads iniciados pela mesma tarefa. Seria uma tragédia! 😟
Logo, a existência da possibilidade de cancelar um contexto em qualquer escopo
seria terrivelmente perigosa e poderia impactar diversos fluxos de forma
imprevisível. Então, caso seja necessário ter um controle fino sobre o tempo de
vida de um determinado fluxo da sua aplicação, a recomendação é criar uma nova
instância do context.Context
. O pacote context
já oferece algumas formas
para criar contextos — vamos dar uma olhada nelas?
Criando novos contextos
É importante lembrar que, exceto pelos métodos context.Background()
e
context.TODO()
, todos as formas de criação de um contexto exigem um contexto base.
Sempre que um contexto base for cancelado, os cancelamentos serão propagados para todos os contextos derivados.
Nessa seção teremos alguns exemplos mais completos que irão utilizar o código abaixo, esses exemplos também contam com um link para execução no Go Playground.
Estrutura dos exemplos
Para alguns dos exemplos a seguir, decidi seguir a vibe de restaurante e criei
duas structs
.
A restaurant
representando o próprio restaurante, com o metodo order
para “receber” os pedidos e encaminhá-los a cozinha:
Gotype restaurant struct { kitchen kitchen } func (r restaurant) order(ctx context.Context, dish string) { fmt.Printf("restaurante: preparando o pedido %q\n", dish) r.kitchen.cook(ctx, dish) <-ctx.Done() // espera o contexto ser cancelado fmt.Printf("restaurante: pedido %q cancelado: %s\n", dish, context.Cause(ctx)) }
A outra, kitchen
, representa a cozinha — e tem o método cook
representando o
ato de cozinhar o prato:
Gotype kitchen struct{} func (k kitchen) cook(ctx context.Context, dish string) { fmt.Printf("cozinha: fingindo que estamos fazendo %q\n", dish) <-ctx.Done() // espera o contexto ser cancelado fmt.Printf("cozinha: parando de fazer %q: %s\n", dish, context.Cause(ctx)) }
Note que nesse exemplo estamos usando o método ctx.Done()
, e a função context.Cause(ctx)
, vamos falar mais sobre elas na seção lidando com o cancelamento
Sem possibilidade de cancelamento
Decidi organizar as funções de criação de contexto pela possibilidade ou não de cancelamento. Vamos iniciar pelas formas que não oferecem nenhum tipo de mecanismo para cancelamento.
Criando contextos básicos — context.Background
O context.Background
cria um novo contexto vazio — ou seja, sem prazos, sem
valores e que não pode ser cancelado.
Ele costuma ser usado como ponto de partida quando precisamos passar um contexto, mas ainda não temos um contexto anterior disponível.
É muito comum usá-lo no início da aplicação (como na função main
), durante a
configuração de serviços ou em testes, onde precisamos de um contexto base
para começar a construir outros contextos a partir dele.
Goctx := context.Background()
Criando contextos provisórios - context.TODO
O context.TODO
pode ser usado quando não se tem certeza de qual outra opção
deve ser usada. A intenção dele é ser apenas um placeholder e deve ser substituído.
Assim como o context.Background
, ele não pode ser cancelado.
Goctx := context.TODO()
Contextos que carregam valores — context.WithValue
Também existe a função context.WithValue
, que permite a criação de um contexto
com valores armazenados internamente.
Gofunc main() { iceCreamPlace := restaurant{kitchen: kitchen{}} // Cria um contexto com valor ctx := context.WithValue(context.Background(), "orderId", "1") dish := "sorvete de cebola" fmt.Printf("cliente: pedindo %q\n", dish) iceCreamPlace.order(ctx, dish) }
Textcliente: pedindo "sorvete de cebola" restaurante: recebendo pedido de "sorvete de cebola" cozinha: preparando "sorvete de cebola" (pedido #1)
AtençãoRecomendo bastante cautela ao utilizar contextos dessa forma, embora seja muito útil para passar agentes de métricas, tracing ou dados de um request — como um request id — entre as diferentes camadas, o abuso dessa opção pode causar problemas de clareza no código.
Essa funcionalidade não deve ser utilizada como um dicionário genérico global.
Criando contextos derivados sem cancelamento — context.WithoutCancel
Disponível a partir do Go 1.21
O context.WithoutCancel
cria um novo contexto a partir de um contexto base, mas continuará ativo, mesmo que o original tenha sido cancelado.
Gofunc main() { iceCreamPlace := restaurant{kitchen: kitchen{}} // Cria um contexto com cancelamento ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Cria um contexto derivado que ignora o cancelamento do pai ctx2 := context.WithoutCancel(ctx) dish := "sorvete de cebola" fmt.Printf("cliente: pedindo %q\n", dish) go func() { iceCreamPlace.order(ctx2, dish) }() fmt.Println("cliente: esperando o pedido ficar pronto") time.Sleep(1 * time.Second) fmt.Printf("cliente: cancelando %q\n", dish) cancel() // Cancela o contexto pai, mas ctx2 não será cancelado time.Sleep(2 * time.Second) fmt.Printf("cliente: verificando status do pedido %q\n", dish) }
Com possibilidade de cancelamento
Criando contextos com cancelamento — context.WithCancel
e context.WithCancelCause
A função context.WithCancel
possui dois retornos. O primeiro é o próprio
contexto, que deve ser repassado nas chamadas em que ele se faz necessário, já o
segundo é uma função de cancelamento que, ao ser chamada, irá cancelar o
contexto criado.
AvisoÉ sempre uma boa prática utilizar o
defer
nas funções de cancelamento, pois ajuda a evitar o vazamento de goroutines.
Gofunc main() { iceCreamPlace := restaurant{kitchen: kitchen{}} ctx, cancel := context.WithCancel(context.Background()) defer cancel() dish := "sorvete de cebola" fmt.Printf("cliente: pedindo %q\n", dish) go func() { iceCreamPlace.order(ctx, dish) }() fmt.Println("cliente: esperando o pedido ficar pronto") time.Sleep(1 * time.Second) fmt.Printf("cliente: cancelando %q\n", dish) cancel() time.Sleep(1 * time.Second) }
Textcliente: pedindo "sorvete de cebola" cliente: esperando o pedido ficar pronto restaurante: preparando o pedido "sorvete de cebola" cozinha: fingindo que estamos fazendo "sorvete de cebola" cliente: cancelando "sorvete de cebola" cozinha: parando de fazer "sorvete de cebola": context canceled restaurante: pedido "sorvete de cebola" cancelado: context canceled
Também é possível evidenciar a causa do cancelamento utilizando a função
context.WithCancelCause
.
Gofunc main() { iceCreamPlace := restaurant{kitchen: kitchen{}} ctx, cancel := context.WithCancelCause(context.Background()) defer cancel(nil) // retorna o erro original: "context canceled" dish := "sorvete de cebola" fmt.Printf("cliente: pedindo %q\n", dish) go func() { iceCreamPlace.order(ctx, dish) }() fmt.Println("cliente: esperando o pedido ficar pronto") time.Sleep(1 * time.Second) cause := errors.New("cancelado pelo cliente") cancel(cause) time.Sleep(1 * time.Second) }
Textcliente: pedindo "sorvete de cebola" cliente: esperando o pedido ficar pronto restaurante: preparando o pedido "sorvete de cebola" cozinha: fingindo que estamos fazendo "sorvete de cebola" cozinha: parando de fazer "sorvete de cebola": cancelado pelo cliente restaurante: pedido "sorvete de cebola" cancelado: cancelado pelo cliente
Com prazo de validade absoluto — context.WithDeadline
e context.WithDeadlineCause
A context.WithDeadline
, que recebe um time.Time
e irá cancelar o
contexto automaticamente após a data informada.
Gofunc main() { iceCreamPlace := restaurant{kitchen: kitchen{}} // Define um deadline para 2 segundos no futuro deadline := time.Now().Add(2 * time.Second) ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() dish := "sorvete de cebola" fmt.Printf("cliente: pedindo %q\n", dish) go func() { iceCreamPlace.order(ctx, dish) }() fmt.Println("cliente: esperando o pedido ficar pronto") time.Sleep(3 * time.Second) // espera mais do que o deadline fmt.Printf("cliente: verificando status do pedido %q\n", dish) }
Textcliente: pedindo "sorvete de cebola" cliente: esperando o pedido ficar pronto restaurante: preparando o pedido "sorvete de cebola" cozinha: fingindo que estamos fazendo "sorvete de cebola" cozinha: parando de fazer "sorvete de cebola": context deadline exceeded restaurante: pedido "sorvete de cebola" cancelado: context deadline exceeded cliente: verificando status do pedido "sorvete de cebola"
Também podemos informar a causa do cancelamento trocando context.WithDeadline
por context.WithDeadlineCause
:
Gofunc main() { iceCreamPlace := restaurant{kitchen: kitchen{}} deadline := time.Now().Add(2 * time.Second) cause := errors.New("hora de levar minha avó pra aula de judô") ctx, cancel := context.WithDeadlineCause(context.Background(), deadline, cause) defer cancel() // restante do código }
Textcliente: pedindo "sorvete de cebola" cliente: esperando o pedido ficar pronto restaurante: preparando o pedido "sorvete de cebola" cozinha: fingindo que estamos fazendo "sorvete de cebola" cozinha: parando de fazer "sorvete de cebola": hora de levar minha avó pra aula de judô restaurante: pedido "sorvete de cebola" cancelado: hora de levar minha avó pra aula de judô cliente: verificando status do pedido "sorvete de cebola"
Com prazo de validade relativo — context.WithTimeout
e context.WithTimeoutCause
A context.WithTimeout
, que recebe um time.Duration
e irá cancelar o
contexto após o período informado.
Gofunc main() { iceCreamPlace := restaurant{kitchen: kitchen{}} // Define um timeout de 2 segundos ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() dish := "sorvete de cebola" fmt.Printf("cliente: pedindo %q\n", dish) go func() { iceCreamPlace.order(ctx, dish) }() fmt.Println("cliente: esperando o pedido ficar pronto") time.Sleep(3 * time.Second) // espera mais do que o timeout fmt.Printf("cliente: verificando status do pedido %q\n", dish) }
Textcliente: pedindo "sorvete de cebola" cliente: esperando o pedido ficar pronto restaurante: preparando o pedido "sorvete de cebola" cozinha: fingindo que estamos fazendo "sorvete de cebola" cozinha: parando de fazer "sorvete de cebola": context deadline exceeded restaurante: pedido "sorvete de cebola" cancelado: context deadline exceeded cliente: verificando status do pedido "sorvete de cebola"
Note que a saída é igual a do exemplo comcontext.WithDeadline
Aqui também é possível informar uma causa:
Gofunc main() { iceCreamPlace := restaurant{kitchen: kitchen{}} // Define um timeout de 2 segundos cause := errors.New("desisti de esperar") ctx, cancel := context.WithTimeoutCause(context.Background(), 2*time.Second, cause) defer cancel() dish := "sorvete de cebola" fmt.Printf("cliente: pedindo %q\n", dish) go func() { iceCreamPlace.order(ctx, dish) }() fmt.Println("cliente: esperando o pedido ficar pronto") time.Sleep(3 * time.Second) // espera mais do que o timeout fmt.Printf("cliente: verificando status do pedido %q\n", dish) }
Textcliente: pedindo "sorvete de cebola" cliente: esperando o pedido ficar pronto restaurante: preparando o pedido "sorvete de cebola" cozinha: fingindo que estamos fazendo "sorvete de cebola" cozinha: parando de fazer "sorvete de cebola": desisti de esperar restaurante: pedido "sorvete de cebola" cancelado: desisti de esperar cliente: verificando status do pedido "sorvete de cebola"
Criando seu próprio contexto
Por se tratar de uma interface, você pode criar sua própria implementação. Pessoalmente não recomendo seguir por esse caminho, pois nesses meus quase 10 anos de Go eu ainda não vi nenhum cenário que as interfaces fornecidas pela biblioteca padrão não foram suficientes.
Lidando com o cancelamento
Não, não estamos falando de redes sociais
Você pode ativamente verificar se o contexto já foi cancelado, essa abordagem é útil para impedir que o fluxo prossiga. A forma mais comum é realizar a verificação ao início da função, mas em alguns momentos pode ser importante verificar ao final, para garantir que o fluxo não irá continuar.
Verificando se o contexto já foi cancelado — ctx.Err
Você pode simplesmente verificar se o retorno de ctx.Err()
é não-nulo. A
função retornará nil
caso o contexto ainda não tenha sido cancelado e algum
erro caso o cancelamento tenha ocorrido.
Go// verificação antes do processamento ocorrer if ctx.Err() != nil { return fmt.Errorf("falha antes de iniciar: %w", ctx.Err()) } // implementação do código // verificação após o processamento ter ocorrido if ctx.Err() != nil { return fmt.Errorf("falha após a execução: %w", ctx.Err()) }
Identificando a causa do cancelamento — context.Cause
Disponível a partir do Go 1.21
A partir do Go 1.21
existe a opção de associar um error
como causa do cancelamento de um contexto
e que pode ser obtido utilizando a função context.Cause
.
Caso o contexto tenha sido cancelado e exista uma causa não-nula, o valor retornado será o erro enviado como causa no momento do cancelamento. Já, se não existir uma causa específica, o valor será o mesmo da chamada ctx.Err()
, que vimos anteriormente.
Goif ctx.Err() != nil { // o contexto foi cancelado, só vamos retornar um erro informando o motivo, ok? return fmt.Errorf("failure before start long process: %w", context.Cause(ctx)) }
Escutando o sinal de cancelamento — ctx.Done
Além da verificação ativa utilizando o ctx.Err()
, também é possível ouvir
um sinal de cancelamento por meio de um canal do tipo <-chan struct{}
,
retornado pela chamada ctx.Done()
.
CanaisOs canais — do inglês channels — são condutores de informação seguros em ambientes concorrentes. Isso significa que podem ser usados por várias goroutines ao mesmo tempo, sem risco de condições de corrida.
Para entender melhor como canais funcionam, recomendo visitar o a seção do tour sobre concorrência:
Tour do Go — Concurrency.
Structs vazias (struct{}
)A
struct{}
é uma estrutura vazia — ela não consome memória. Por isso, o canal retornado porctx.Done()
é usado apenas para sinalizar o cancelamento, sem carregar dados.Saiba mais sobre structs vazias em:
The empty struct — Dave Cheney
Quando a chamada <-ctx.Done()
é feita, o código aguarda o recebimento através
do canal, bloqueando a execução da goroutine até receber algum conteúdo.
Gopackage main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Goroutine que aguarda o cancelamento do contexto go func() { <-ctx.Done() // aguardando o recebimento da mensagem de cancelamento fmt.Println("contexto cancelado após recebimento do canal!") }() fmt.Println("fazendo algo importante...") time.Sleep(1 * time.Second) fmt.Println("cancelando o contexto") cancel() time.Sleep(1 * time.Second) // aguardando callback ser executado }
Textfazendo algo importante... cancelando o contexto contexto cancelado após recebimento do canal!
Pela natureza bloqueante da chamada, geralmente usamos uma cláusula select
para escolher entre o resultado do ctx.Done()
e algum outro canal.
Gopackage main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Goroutine que aguarda o cancelamento do contexto go func() { fmt.Println("goroutine: aguardando cancelamento ou conclusão...") select { case <-ctx.Done(): // Escuta o cancelamento do contexto fmt.Println("goroutine: contexto cancelado!") case <-time.After(2 * time.Second): // Simula uma tarefa longa fmt.Println("goroutine: tarefa concluída!") } }() fmt.Println("fazendo algo importante...") time.Sleep(1 * time.Second) fmt.Println("cancelando o contexto") cancel() time.Sleep(1 * time.Second) // Aguarda para garantir que a goroutine finalize }
Textfazendo algo importante... goroutine: aguardando cancelamento ou conclusão... cancelando o contexto goroutine: contexto cancelado!
Usando callbacks para tratar um contexto cancelado — context.AfterFunc
Disponível a partir do Go 1.21
A função context.AfterFunc(ctx Context, f func())
recebe um context ctx
, que
ao ser cancelado executa a função f
. Dessa forma, a função f
age como um
callback
acionado no cancelamento, útil em tarefas paralelas que não precisam retornar um
erro — mas precisam fazer algum tratamento quando o contexto for cancelado.
Essa chamada retorna uma func() bool
que pode ser chamada para desfazer a
associação dela com o contexto ctx
, fazendo com que ela não seja mais chamada caso o contexto seja cancelado. Ela irá retornar true
.
Gopackage main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Callback que será chamado quando o contexto for cancelado callback := func() { fmt.Println("callback: contexto cancelado!") } stop := context.AfterFunc(ctx, callback) defer stop() callback2 := func() { fmt.Println("callback2: não serei executado") } stop2 := context.AfterFunc(ctx, callback2) fmt.Println("Conseguimos cancelar o callback2?", stop2()) fmt.Println("Conseguimos cancelar o callback2?", stop2(), "(já está cancelado)") cancel() time.Sleep(1 * time.Second) // aguardando callbacks serem executados }
TextConseguimos cancelar o callback2? true Conseguimos cancelar o callback2? false (já está cancelado) callback: contexto cancelado!
Referências e material adicional
Estamos chegando ao final e não poderia deixar aqui algumas sugestões de material para você consolidar e até se aprofundar sobre concorrência, contextos e goroutines. Todos eles estão em inglês, mas você pode recorrer ao Google Tradutor ou alguma IA.
- A Tour of Go: Concurrency é um tour interativo sobre as principais features do Go, esse link leva direto para a seção sobre concorrência.
- Go Concurrency Patterns: Pipelines and Cancelation explica o padrão de cancelamento de tarefas utilizando canais.
- Go Concurrency Patterns: Context introduz as funcionalidades do pacote
context
. - Context and Struct esclarece porque não é uma boa ideia passar contextos dentro de uma
struct
. - Learn Go with tests: Contexts é uma abordagem que introduz os contextos com uma abordagem prática usando TDD (Test Driven Development).
- The Complete Guide to Context in Golang: Efficient Concurrency Management além de apresentar o tema, se aprofunda um pouco em alguns cenários como requesições HTTP e conexão com banco de dados.
- Graceful Shutdown in Go: Practical Patterns apresenta padrões utilizando canais e contexts para encerrar o ciclo de vida de forma controlada.
Conclusão
Talvez você já tenha lido a documentação do pacote context anteriormente e sentido dificuldades em entender de quando e onde os contextos podem ou devem ser usados. Isso pode ter acontecido porque a documentação se concentra em informar quais funcionalidades existem, mas não os casos de uso prático em que elas se aplicam.
Com esse artigo, foquei em tentar introduzir o tema com alguns exemplos práticos e analogias — e espero que isso tenha te ajudado a entender melhor sobre o tema.
Se você tiver dúvidas, sugestões ou apenas quiser trocar uma ideia, sinta-se à vontade para entrar em contato através dos comentários ou pelos links abaixo:
Estou sempre aberto a feedbacks e novas ideias! 🚀
Até uma próxima 👋!