Posts / [PT] Entendendo o package Context em Go
[PT] Entendendo o package Context em Go
Nos últimos meses tenho voltado meus esforços para o estudo da linguagem Go. Confesso que não é meu primeiro contato com a linguagem – lá em 2017 tive a oportunidade de apresentar um minicurso introdutório em um simpósio universitário. Apesar de não ser uma novidade, precisei revisitar alguns conceitos. O pacote context.Context é um deles. Então, let's go!

Olhando para a documentação oficial, temos o seguinte trecho:
“Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes”.
Ou seja, o Context permite a criação de prazos e valores de escopo de execução aos processos em que ele é compartilhado. Essa mágica acontece pelo fato que o Context usa channels para enviar sinais aos processos, e esses processos por sua vez, escutam essas instruções controlando ações de parada.
Um exemplo clássico de seu uso é em situações de requisições para API’s externas onde queremos delimitar um tempo de espera pela resposta. Também existem outras situações que podemos nos beneficiar do seu uso, tais como controlar o tempo limite de processamento de instruções e determinar prazo de execução de query ao banco de dados, por exemplo. Enfim, são várias as possibilidades.
Iniciando um Context#
Antes de começarmos a detalhar os tipos de Context, é importante entendermos como se dá a sua inicialização. Todo Context para ser usado, deve ser inicializado vazio em algum momento do ciclo de vida. A própria documentação do Go desencoraja passar nil como parâmetro em situações que o requerem. Existem duas formas de inicializá-lo - context.Background() e context.TODO():
context.Background()#
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.Background()
fmt.Println(ctx)
}
O context.Backgroud() nos retorna um Context não nulo e vazio. Normalmente é usado pela função principal ou entrypoint do projeto.
context.TODO()#
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.TODO()
fmt.Println(ctx)
}
Assim como seu antecessor, o context.TODO() também nos retorna um Context não nulo e vazio. A principal diferença é que o seu uso se dá quando não estiver claro o contexto que se deve usar ou ele ainda não estiver disponível.
Tipos de Context#
Existem basicamente três tipos de Context:
- Cancellation Signals
- Deadline
- Request-scoped values
Cancellation Signals#
O tipo Cancellation Signals define um Context que pode ter sua execução interrompida através de um comando. Neste caso, a instrução segue o fluxo normal até que em certo ponto do código seja dada a sua parada. A seguir, um exemplo de implementação:
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
go executeFunction(ctx)
time.Sleep(3 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}
func executeFunction(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Stopping code execution...")
return
default:
time.Sleep(1 * time.Second)
fmt.Println("Running at", time.Now())
}
}
}
Antes de explicar a função main(), vamos entender primeiro a função executeFunction(), pois ela irá receber instruções do Context. Ao receber um parâmetro do tipo context.Context fazemos um loop infinito, onde, assim que receber o sinal de parada vindo do contexto, ela irá apresentar o texto “Stopping code execution...” e terminará sua execução através de um return explícito. Caso contrário, seguirá sua execução apresentando “Running at [horário]”. O comando select funciona como uma espécie de switch-case, porém aplicado ao sinais provenientes de channels, como é o caso de Contexts. Como percebemos, o sinal de parada no código é dado pela instrução ctx.Done().
Retornando para a função main(), a primeira coisa que fazemos é definir um Context através do context.Backgroud(). Após isso, usamos a função context.WithCancel() passando o nosso contexto base. Ele nos devolve um novo contexto e uma segunda instrução que chamamos de cancel. Ela é essencial para definir a parada das instruções que estão ligadas a este contexto. Enquanto executamos a função executeFunction() de forma assíncrona, esperamos três segundos para forçar seu cancelamento com a instrução cancel() - aquela mesmo que recebemos do contexto :) .
O resultado da execução será semelhante ao log abaixo:
Running at 2022-10-09 16:15:46.80042576 -0300 -03 m=+1.000598497
Running at 2022-10-09 16:15:47.801454038 -0300 -03 m=+2.001626779
Running at 2022-10-09 16:15:48.801876258 -0300 -03 m=+3.002048999
Stopping code execution...
Deadline#
Como o próprio termo sugere, um Context do tipo Deadline delimita o tempo de execução de uma instrução ou processo. Podemos então informar para a aplicação até quando determinado código pode demorar. Temos duas funções diferentes para trabalhar com Deadline:
context.WithDeadline()- É definido um valor de tempo fixo.- Exemplo: o contexto tem até as 22h do dia 09 de outubro de 2022 para ser executado.
context.WithTimeout()- É definido um valor de tempo a partir do instante que foi instanciado.- Exemplo: o contexto tem 4 minutos a partir de agora para ser executado.
context.WithDeadline()#
Vamos então pegar o mesmo código anterior e alterar alguns trechos:
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx := context.Background()
deadline := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
executeFunction(ctx)
}
func executeFunction(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Stopping code execution...")
return
default:
time.Sleep(1 * time.Second)
fmt.Println("Running at", time.Now())
}
}
}
Instanciamos uma nova variável deadline somando três segundos ao tempo de agora e passamos ela para a função context.WithDeadLine(). Dessa forma a instrução da função executeFunction() será interrompida após o intervalo de três segundos.
O resultado da execução do código acima será semelhante a seguinte saída:
Running at 2022-10-09 17:30:50.492897161 -0300 -03 m=+1.000190057
Running at 2022-10-09 17:30:51.493583512 -0300 -03 m=+2.000876359
Running at 2022-10-09 17:30:52.493694606 -0300 -03 m=+3.000987449
Running at 2022-10-09 17:30:53.493795753 -0300 -03 m=+4.001088530
Stopping code execution…
context.WithTimeout()#
Podemos usar a função context.WithTimeout() para produzir o mesmo comportamento. Basta alterarmos as linhas 11 e 12 por:
deadline := 3 * time.Second
ctx, cancel := context.WithTimeout(ctx, deadline)
A diferença, para o código anterior é que, agora, não precisamos mais fazer o cálculo do instante atual somado com os três segundos. A saída será a mesma.
Um ponto curioso nessa estrutura é que executamos também o cancel(), porém com a palavra-chave defer para assegurar que ele será a última coisa a ser executada. Apesar de estarmos delimitando o tempo de execução, podemos também antecipar o cancelamento da instrução a qualquer momento se sobrepondo a regra do limite de tempo, assim como explicado na seção Cancellation Signals.
Request-scoped value#
Além de controlar tempo de execução e parada, com Context também é possível passar valores no estilo chave-valor para instruções filhas. Esse conceito é importante pois ele funciona independente se a chamada para as instruções é síncrona ou assíncrona. Esses valores ficam salvos dentro do contexto e são imutáveis. Vamos detalhar o código abaixo um modelo síncrono:
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "sessionId", "Session123")
showValue(ctx)
}
func showValue(ctx context.Context) {
fmt.Println(ctx.Value("sessionId"))
addKeyValue(&ctx, "typeError", "ErrorType123")
fmt.Println(ctx.Value("typeError"))
}
func addKeyValue(ctx *context.Context, key interface{}, value interface{}) {
*ctx = context.WithValue(*ctx, key, value)
}
Definimos o contexto inicial vazio com context.Background(). Após isso, criamos um novo contexto a partir do inicial com context.WithValue() e já também adicionamos valores fictícios ao contexto. Como esses valores ficam guardados no Context, podemos acessá-los em outra parte no programa com a função ctx.Value() passando a chave correspondente. Caso essa chave não exista o retorno é nil. Também é possível adicionar novos valores a um contexto criando um context.WithValues() passando o contexto anterior e atualizando ele próprio.
Demais cenários#
Os casos de uso do Context não se limitam aos aqui apresentados. Atualmente, as bibliotecas em Go, sejam nativas ou não requerem passar Context em suas instruções. O objetivo desse post foi entender a sua base e seus usos comuns.
Até a próxima :)
Referências#
comentários