O pacote context
Esta aula aborda o pacote context da linguagem Go, explicando como gerenciar cancelamentos, timeouts e deadlines em operações concorrentes. A propagação de contextos e boas práticas são discutidas com exemplos práticos para garantir código eficiente e seguro.
O pacote context é uma das ferramentas mais importantes para controle de concorrência em Go. Ele permite que você propague sinais de cancelamento, timeouts e deadlines através de uma cadeia de chamadas, especialmente útil em servidores HTTP, operações de banco de dados e chamadas a APIs externas. Nesta aula, vamos explorar os principais usos do pacote context e como ele pode tornar seu código mais robusto e eficiente.
Contextos são imutáveis e devem ser passados como primeiro argumento em funções que podem ser canceladas ou que possuem prazo. A árvore de contextos é criada a partir de um contexto raiz (context.Background() ou context.TODO()) e derivada com funções como context.WithCancel, context.WithTimeout e context.WithDeadline.
Cancelamento
Cancelamento é a capacidade de sinalizar para goroutines que elas devem parar de executar o trabalho atual. Isso evita desperdício de recursos e permite respostas rápidas a interrupções. O cancelamento é feito através do canal Done() do contexto, que é fechado quando a função de cancelamento é chamada.
Exemplo prático: uma operação que pode ser cancelada pelo usuário ou por outro componente. Vamos criar um contexto cancelável e usar select para ouvir o cancelamento.
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // importante: sempre chamar cancel para liberar recursos
go func() {
time.Sleep(2 * time.Second)
cancel() // cancela o contexto após 2 segundos
}()
select {
case <-time.After(5 * time.Second):
fmt.Println("Operação concluída")
case <-ctx.Done():
fmt.Println("Operação cancelada:", ctx.Err())
}
}
No exemplo, a goroutine cancela o contexto após 2 segundos. O select espera até 5 segundos ou até o cancelamento. Como o cancelamento ocorre primeiro, a saída será "Operação cancelada: context canceled".
Timeouts e deadlines
Timeouts e deadlines são variações do cancelamento que ocorrem automaticamente após um período de tempo. context.WithTimeout cria um contexto que será cancelado após a duração especificada. context.WithDeadline define um ponto absoluto no tempo para o cancelamento.
Exemplo de timeout: uma requisição HTTP que deve falhar se demorar mais que 1 segundo.
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
// Simula uma operação longa
select {
case <-time.After(3 * time.Second):
fmt.Println("Operação concluída")
case <-ctx.Done():
fmt.Println("Timeout:", ctx.Err())
}
}
A saída será "Timeout: context deadline exceeded" porque a operação leva 3 segundos, mas o timeout é de 1 segundo.
Deadlines são úteis quando você tem um prazo absoluto, como uma data específica. Exemplo:
deadline := time.Now().Add(2 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
Propagação
Contextos são projetados para serem propagados através da árvore de chamadas. Quando você cria um contexto filho a partir de um pai, o cancelamento do pai propaga-se para todos os filhos. Isso permite que um sinal de cancelamento em um nível superior cancele todas as operações descendentes.
Exemplo: uma função que faz duas chamadas concorrentes e cancela ambas se uma falhar.
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
operacao(ctx, "A", 3*time.Second)
}()
go func() {
defer wg.Done()
operacao(ctx, "B", 1*time.Second)
}()
// Cancela após 2 segundos
time.Sleep(2 * time.Second)
cancel()
wg.Wait()
fmt.Println("Fim")
}
func operacao(ctx context.Context, nome string, duracao time.Duration) {
select {
case <-time.After(duracao):
fmt.Printf("Operação %s concluída\n", nome)
case <-ctx.Done():
fmt.Printf("Operação %s cancelada: %v\n", nome, ctx.Err())
}
}
Neste exemplo, a operação B termina em 1 segundo antes do cancelamento, enquanto a operação A é cancelada após 2 segundos. Ambas recebem o sinal de cancelamento porque o contexto pai foi cancelado.
Boas práticas
Ao usar contextos, siga estas boas práticas:
- Passe o contexto como primeiro argumento em funções que podem ser canceladas ou que precisam de deadline.
- Nunca armazene contextos em structs; passe-os explicitamente.
- Sempre chame a função de cancelamento retornada por
WithCancel,WithTimeoutouWithDeadlinepara liberar recursos, geralmente comdefer cancel(). - Use
context.Background()para contextos raiz econtext.TODO()quando você ainda não sabe qual contexto usar. - Evite passar
nilcomo contexto; usecontext.TODO()se necessário. - Não modifique o contexto depois de criá-lo; contextos são imutáveis.
- Valores associados ao contexto (com
context.WithValue) devem ser usados com moderação, apenas para dados de escopo de requisição, como IDs de tracing.
Exemplo de uso correto com valores:
type key string
const userIDKey key = "userID"
func handler(ctx context.Context) {
userID := ctx.Value(userIDKey).(string)
// usa userID
}
func main() {
ctx := context.WithValue(context.Background(), userIDKey, "12345")
handler(ctx)
}
Referências
- Documentação oficial do pacote context
- Go Blog: Context
- Go by Example: Context
- Mat Ryer: Context in Go
- DigitalOcean: How To Use Contexts in Go
Exercícios
Crie uma função que receba um contexto e um nome de arquivo, e simule a leitura de um arquivo que leva 2 segundos. Se o contexto for cancelado antes, a função deve retornar imediatamente com um erro.
✓ Resposta:func readFile(ctx context.Context, filename string) ([]byte, error) { select { case <-time.After(2 * time.Second): // Simula leitura return []byte("conteúdo"), nil case <-ctx.Done(): return nil, ctx.Err() } }Escreva um programa que faz duas requisições HTTP concorrentes com timeout de 1 segundo cada. Use contextos com timeout. Se uma requisição falhar por timeout, a outra deve ser cancelada.
✓ Resposta:package main import ( "context" "fmt" "net/http" "sync" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() reqCtx, reqCancel := context.WithTimeout(ctx, 1*time.Second) defer reqCancel() req, _ := http.NewRequestWithContext(reqCtx, "GET", "http://example.com", nil) _, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("Erro na requisição 1:", err) cancel() // cancela a outra } }() go func() { defer wg.Done() reqCtx, reqCancel := context.WithTimeout(ctx, 1*time.Second) defer reqCancel() req, _ := http.NewRequestWithContext(reqCtx, "GET", "http://example.org", nil) _, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("Erro na requisição 2:", err) cancel() } }() wg.Wait() }Implemente uma função que executa uma tarefa em loop até que o contexto seja cancelado. A função deve processar itens de um canal e parar quando o contexto for cancelado, fechando o canal.
✓ Resposta:func worker(ctx context.Context, jobs <-chan int, results chan<- int) { for { select { case <-ctx.Done(): close(results) return case job, ok := <-jobs: if !ok { close(results) return } // processa job results <- job * 2 } } }Crie um servidor HTTP que usa contextos para cancelar requisições longas. Use
http.TimeoutHandlerou implemente manualmente comcontext.WithTimeout.✓ Resposta:package main import ( "context" "fmt" "net/http" "time" ) func slowHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() select { case <-time.After(5 * time.Second): fmt.Fprintln(w, "Resposta lenta") case <-ctx.Done(): http.Error(w, "Request cancelled", http.StatusRequestTimeout) } } func main() { handler := http.HandlerFunc(slowHandler) timeoutHandler := http.TimeoutHandler(handler, 2*time.Second, "Timeout") http.ListenAndServe(":8080", timeoutHandler) }Escreva uma função que propaga valores através do contexto (como um ID de correlação) e os utiliza em funções aninhadas. Demonstre o uso de
context.WithValue.✓ Resposta:package main import ( "context" "fmt" ) type key string const traceIDKey key = "traceID" func main() { ctx := context.WithValue(context.Background(), traceIDKey, "abc123") process(ctx) } func process(ctx context.Context) { traceID := ctx.Value(traceIDKey).(string) fmt.Println("Trace ID:", traceID) subProcess(ctx) } func subProcess(ctx context.Context) { traceID := ctx.Value(traceIDKey).(string) fmt.Println("Subprocess trace ID:", traceID) }