O pacote context
Nesta aula, exploramos o pacote context da linguagem Go, que permite controlar cancelamentos, timeouts e deadlines em operações concorrentes. Aprendemos como propagar contextos entre goroutines e boas práticas para seu uso eficiente.
O pacote context é uma das ferramentas mais importantes em Go para gerenciar operações de longa duração, como chamadas HTTP, consultas a banco de dados ou qualquer tarefa concorrente. Ele fornece uma maneira padronizada de transmitir sinais de cancelamento, definir prazos e compartilhar valores entre goroutines. Dominar o pacote context é essencial para escrever código Go robusto e eficiente.
Nesta aula, vamos explorar os principais usos do pacote: cancelamento, timeouts e deadlines, propagação de contextos e boas práticas. Cada tópico será acompanhado de exemplos práticos para que você entenda como aplicar esses conceitos no dia a dia.
Cancelamento
O cancelamento é um dos recursos mais poderosos do pacote context. Ele permite que uma goroutine sinalize para outras goroutines que devem parar seu trabalho, liberando recursos rapidamente. Isso é útil, por exemplo, quando um usuário cancela uma requisição ou quando um serviço precisa ser desligado.
Para criar um contexto cancelável, usamos context.WithCancel, que retorna um novo contexto e uma função de cancelamento. Quando a função é chamada, o contexto é cancelado e todos os contextos derivados dele também são cancelados. As goroutines que escutam o canal Done() do contexto podem detectar o cancelamento e agir adequadamente.
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // boa prática: garantir cancelamento ao final
go func() {
select {
case <-ctx.Done():
fmt.Println("Goroutine cancelada")
return
case <-time.After(2 * time.Second):
fmt.Println("Trabalho concluído")
}
}()
time.Sleep(1 * time.Second)
cancel() // cancela o contexto antes do timeout
time.Sleep(1 * time.Second)
}No exemplo acima, a goroutine executa um trabalho que pode levar até 2 segundos. Após 1 segundo, chamamos cancel(), o que faz com que o canal ctx.Done() seja fechado, e a goroutine imediatamente imprime "Goroutine cancelada" e retorna. Sem o cancelamento, ela esperaria os 2 segundos desnecessariamente.
Timeouts e deadlines
Além do cancelamento manual, podemos definir timeouts ou deadlines automáticos usando context.WithTimeout e context.WithDeadline. Um timeout é uma duração máxima para a operação, enquanto um deadline é um momento específico no tempo em que o contexto será cancelado automaticamente.
Esses mecanismos são ideais para evitar que operações fiquem bloqueadas indefinidamente. Por exemplo, ao fazer uma requisição HTTP, podemos definir um timeout de 5 segundos. Se a resposta não chegar nesse período, o contexto é cancelado e podemos tratar o erro adequadamente.
package main
import (
"context"
"fmt"
"time"
)
func operacaoLenta(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err() // retorna o motivo do cancelamento
case <-time.After(10 * time.Second):
fmt.Println("Operação concluída")
return nil
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err := operacaoLenta(ctx)
if err != nil {
fmt.Println("Erro:", err) // imprime "context deadline exceeded"
}
}Neste exemplo, a operação levaria 10 segundos, mas definimos um timeout de 3 segundos. Quando o timeout expira, o contexto é cancelado e ctx.Err() retorna context deadline exceeded. A função então retorna o erro, e podemos tratá-lo adequadamente.
Para usar deadlines, podemos fazer:
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()Propagação
Uma das características mais importantes do pacote context é a propagação: contextos podem ser aninhados, formando uma árvore. Quando um contexto pai é cancelado, todos os contextos filhos (derivados) também são cancelados. Isso permite que um cancelamento em um nível superior se propague para todas as operações dependentes.
Por exemplo, ao receber uma requisição HTTP, o servidor pode criar um contexto raiz para aquela requisição. Se o cliente desconectar, o contexto pode ser cancelado, e todas as goroutines que realizam tarefas relacionadas (como consultas ao banco, chamadas a APIs externas) serão notificadas e podem parar.
package main
import (
"context"
"fmt"
"time"
)
func tarefa(ctx context.Context, nome string) {
select {
case <-ctx.Done():
fmt.Printf("%s cancelada\n", nome)
case <-time.After(2 * time.Second):
fmt.Printf("%s concluída\n", nome)
}
}
func main() {
ctxPai, cancel := context.WithCancel(context.Background())
defer cancel()
ctxFilho1, _ := context.WithCancel(ctxPai)
ctxFilho2, _ := context.WithCancel(ctxPai)
go tarefa(ctxFilho1, "Tarefa 1")
go tarefa(ctxFilho2, "Tarefa 2")
time.Sleep(1 * time.Second)
cancel() // cancela o pai, que cancela os filhos
time.Sleep(1 * time.Second)
}Ao cancelar o contexto pai, ambos os contextos filhos são cancelados automaticamente, e as goroutines imprimem "cancelada". Isso demonstra como a propagação simplifica o gerenciamento de múltiplas operações concorrentes.
Boas práticas
O uso correto do pacote context é fundamental para escrever código Go idiomático e eficiente. Aqui estão algumas boas práticas:
- Passe context como primeiro parâmetro: Por convenção, o contexto deve ser o primeiro argumento de funções que o utilizam. Exemplo:
func minhaFuncao(ctx context.Context, ...). - Não armazene contextos em structs: Contextos devem ser passados explicitamente, não armazenados como campos. Isso evita vazamentos e facilita a rastreabilidade.
- Sempre chame a função de cancelamento: Ao usar
WithCancel,WithTimeoutouWithDeadline, usedefer cancel()para garantir que o contexto seja cancelado, liberando recursos. - Use
context.Background()como raiz: Para contextos de alto nível (como nomain), usecontext.Background(). Para testes, usecontext.TODO()quando o contexto ainda não estiver definido. - Não use context para passar parâmetros opcionais: Embora seja possível armazenar valores no contexto com
context.WithValue, isso deve ser usado com moderação, apenas para dados de escopo de requisição (como IDs de correlação). Prefira parâmetros explícitos. - Verifique
ctx.Done()em loops: Em operações que envolvem loops, verifique regularmente se o contexto foi cancelado para evitar trabalho desnecessário.
Exemplo de uso com context.WithValue (uso moderado):
type chave string
const userIDKey chave = "userID"
func handler(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), userIDKey, "123")
processar(ctx)
}
func processar(ctx context.Context) {
userID := ctx.Value(userIDKey).(string)
fmt.Println("User ID:", userID)
}Referências
- Documentação oficial do pacote context
- Artigo do blog Go: Go Concurrency Patterns: Context
- Effective Go: Concurrency
- DigitalOcean: How To Use Contexts in Go
- Mat Ryer: Context is for cancelation
Exercícios
-
Crie uma função que simule uma operação de longa duração (por exemplo, um loop que conta de 1 a 10 com pausas de 1 segundo) e que respeite um contexto cancelável. Use
context.WithCancele cancele o contexto após 3 segundos. A função deve parar imediatamente ao detectar o cancelamento.✓ Resposta:package main import ( "context" "fmt" "time" ) func operacaoLonga(ctx context.Context) { for i := 1; i <= 10; i++ { select { case <-ctx.Done(): fmt.Println("Operação cancelada") return default: fmt.Println(i) time.Sleep(1 * time.Second) } } } func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() go operacaoLonga(ctx) time.Sleep(3 * time.Second) cancel() time.Sleep(1 * time.Second) // espera a goroutine imprimir } -
Escreva uma função que faça uma requisição HTTP com timeout de 2 segundos. Se a requisição exceder o timeout, a função deve retornar um erro. Use
context.WithTimeoute a bibliotecanet/http.✓ Resposta:package main import ( "context" "fmt" "net/http" "time" ) func requisicaoComTimeout(url string) (*http.Response, error) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } return resp, nil } func main() { resp, err := requisicaoComTimeout("https://httpbin.org/delay/5") // demora 5s if err != nil { fmt.Println("Erro:", err) return } defer resp.Body.Close() fmt.Println("Status:", resp.Status) } -
Implemente um programa que crie uma árvore de contextos: um contexto pai com timeout de 4 segundos, e dois contextos filhos. Cada filho deve executar uma tarefa que leva 3 segundos. Mostre que, se o pai for cancelado, os filhos também são cancelados.
✓ Resposta:package main import ( "context" "fmt" "time" ) func tarefa(ctx context.Context, nome string) { select { case <-ctx.Done(): fmt.Printf("%s cancelada: %v\n", nome, ctx.Err()) case <-time.After(3 * time.Second): fmt.Printf("%s concluída\n", nome) } } func main() { ctxPai, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() ctxFilho1, _ := context.WithCancel(ctxPai) ctxFilho2, _ := context.WithCancel(ctxPai) go tarefa(ctxFilho1, "Filho1") go tarefa(ctxFilho2, "Filho2") time.Sleep(5 * time.Second) // espera o timeout do pai // Como o pai tem timeout de 4s, ele cancela automaticamente, cancelando os filhos // Se quiser cancelar manualmente, chame cancel() antes } -
Crie uma função que receba um contexto e um número de iterações. A função deve executar um loop que processa cada iteração (por exemplo, imprimir o número) e verificar o contexto a cada iteração. Se o contexto for cancelado, a função deve parar imediatamente e retornar o erro do contexto.
✓ Resposta:package main import ( "context" "fmt" "time" ) func processar(ctx context.Context, iteracoes int) error { for i := 0; i < iteracoes; i++ { select { case <-ctx.Done(): return ctx.Err() default: fmt.Println("Processando", i) time.Sleep(500 * time.Millisecond) } } return nil } func main() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() err := processar(ctx, 10) if err != nil { fmt.Println("Erro:", err) } } -
Escreva um programa que use
context.WithValuepara passar um ID de requisição através de uma cadeia de funções. Crie três funções que se chamam em sequência, cada uma adicionando mais informações ao contexto, e ao final imprima todos os valores.✓ Resposta:package main import ( "context" "fmt" ) type chave string const ( reqIDKey chave = "requestID" userIDKey chave = "userID" tokenKey chave = "token" ) func funcao1(ctx context.Context) context.Context { ctx = context.WithValue(ctx, reqIDKey, "abc123") return funcao2(ctx) } func funcao2(ctx context.Context) context.Context { ctx = context.WithValue(ctx, userIDKey, "42") return funcao3(ctx) } func funcao3(ctx context.Context) context.Context { ctx = context.WithValue(ctx, tokenKey, "token-secreto") return ctx } func main() { ctx := context.Background() ctxFinal := funcao1(ctx) fmt.Println("Request ID:", ctxFinal.Value(reqIDKey)) fmt.Println("User ID:", ctxFinal.Value(userIDKey)) fmt.Println("Token:", ctxFinal.Value(tokenKey)) }