Context em handlers web
Nesta aula, exploramos o uso do pacote context em handlers web na linguagem Go. Aprendemos a propagar context, lidar com cancelamento de requisições, armazenar valores e aplicar timeouts para criar aplicações web robustas e eficientes.
Quando desenvolvemos aplicações web em Go, o pacote context é uma ferramenta essencial para gerenciar o ciclo de vida de uma requisição. Ele permite que você propague valores, sinais de cancelamento e prazos (deadlines) através da cadeia de chamadas, desde o handler até as camadas mais profundas de acesso a dados. Sem o uso adequado de context, sua aplicação pode sofrer com vazamentos de goroutines, requisições lentas que nunca terminam e dificuldade em rastrear informações importantes da requisição, como IDs de correlação.
Nesta aula, vamos mergulhar nos conceitos fundamentais do pacote context no contexto de handlers web. Você aprenderá a propagar context corretamente, a lidar com cancelamentos de requisições por parte do cliente ou do servidor, a armazenar e recuperar valores dentro do context, e a configurar timeouts para evitar que operações demorem demais. Ao final, você terá as habilidades necessárias para escrever handlers web mais robustos, escaláveis e seguros.
Propagando context
O contexto em Go é uma interface que carrega prazos, cancelamentos e valores. Em aplicações web, cada requisição HTTP tem seu próprio contexto, criado pelo servidor. Esse contexto é passado para o handler e deve ser propagado para todas as funções que fazem trabalho relacionado àquela requisição, como consultas ao banco de dados, chamadas a APIs externas ou processamento de arquivos.
Para propagar o context, você deve passá-lo como primeiro argumento para funções que o aceitam. Por exemplo, funções que interagem com o banco de dados geralmente têm a assinatura func Query(ctx context.Context, query string) (*Result, error). Ao chamar essas funções dentro do handler, você repassa o r.Context() da requisição. Isso garante que, se a requisição for cancelada (por exemplo, o cliente desconectar), todas as operações relacionadas também sejam canceladas.
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Passa o contexto para uma função de banco de dados
result, err := db.Query(ctx, "SELECT * FROM users")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// ... usa o resultado
}
func (db *DB) Query(ctx context.Context, query string) (*Result, error) {
// Simula uma consulta demorada
select {
case <-ctx.Done():
return nil, ctx.Err()
case result := <-db.execute(query):
return result, nil
}
}A propagação correta do context é crucial para evitar vazamentos de recursos. Se você ignorar o context e usar um contexto de fundo (context.Background()) em operações que deveriam ser canceláveis, a goroutine pode ficar presa indefinidamente, consumindo memória e CPU. Além disso, ao propagar o context, você permite que o servidor responda rapidamente a cancelamentos e aplique timeouts de forma consistente.
Cancelamento de requisições
O cancelamento de requisições é uma funcionalidade importante em aplicações web, especialmente quando o cliente desconecta ou quando o servidor precisa interromper um processamento longo. O pacote context oferece mecanismos para detectar e reagir a cancelamentos. O contexto da requisição (r.Context()) é cancelado automaticamente quando o cliente fecha a conexão ou quando o servidor decide interromper a requisição.
Para lidar com cancelamento, você pode usar o método Done() do context, que retorna um canal que é fechado quando o contexto é cancelado. Dentro de funções que executam trabalho, você pode selecionar entre o canal de cancelamento e o canal de conclusão da operação. Se o cancelamento ocorrer, você deve retornar um erro apropriado, geralmente context.Canceled ou context.DeadlineExceeded.
func longOperation(ctx context.Context) (string, error) {
resultCh := make(chan string)
go func() {
// Simula um trabalho demorado
time.Sleep(5 * time.Second)
resultCh <- "done"
}()
select {
case <-ctx.Done():
return "", ctx.Err()
case result := <-resultCh:
return result, nil
}
}
func handler(w http.ResponseWriter, r *http.Request) {
result, err := longOperation(r.Context())
if err != nil {
// Se o cancelamento aconteceu, não escreva resposta
if errors.Is(err, context.Canceled) {
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintln(w, result)
}Além de detectar cancelamentos, você também pode criar contextos canceláveis manualmente usando context.WithCancel. Isso é útil quando você deseja cancelar uma operação a partir do próprio servidor, por exemplo, se um novo pedido substituir o anterior. No entanto, em handlers web, o contexto da requisição já é cancelável, então você raramente precisa criar um novo contexto cancelável, a menos que esteja gerenciando suboperações.
Valores
O pacote context também permite armazenar valores que podem ser acessados por qualquer função que receba o mesmo context. Isso é útil para passar informações como IDs de usuário, tokens de autenticação, IDs de correlação, etc., sem precisar adicionar parâmetros extras a cada função. No entanto, é importante usar essa funcionalidade com moderação e apenas para dados que são realmente necessários em toda a cadeia de chamadas.
Para criar um contexto com valores, use context.WithValue, que retorna um novo contexto que contém o valor. Para recuperar, use ctx.Value(key). A chave deve ser um tipo próprio, não um tipo básico, para evitar colisões. Por exemplo, defina um tipo type contextKey string e use constantes para as chaves.
type contextKey string
const userIDKey contextKey = "userID"
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simula autenticação
userID := "12345"
ctx := context.WithValue(r.Context(), userIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func handler(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value(userIDKey).(string)
if !ok {
http.Error(w, "user not found", http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "Hello user %s", userID)
}Embora seja tentador usar context para passar muitos valores, isso pode tornar o código menos legível e dificultar os testes. Prefira passar parâmetros explícitos quando a função só precisa de alguns poucos dados. O context deve ser reservado para dados que são verdadeiramente transversais, como identificadores de rastreamento ou informações de autenticação.
Timeouts
Timeouts são essenciais para evitar que uma requisição web fique pendente por muito tempo. O pacote context permite configurar prazos (deadlines) ou durações de timeout. Quando o tempo expira, o context é cancelado automaticamente, e as funções que o utilizam devem parar de trabalhar e retornar um erro.
Você pode criar um contexto com timeout usando context.WithTimeout ou context.WithDeadline. No contexto de uma requisição web, você pode usar o http.TimeoutHandler para envolver um handler e aplicar um timeout global. No entanto, é mais flexível aplicar timeouts dentro das operações individuais, como consultas ao banco de dados, usando o context da requisição.
func handler(w http.ResponseWriter, r *http.Request) {
// Cria um contexto com timeout de 2 segundos
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel() // Garante que o contexto seja cancelado após o handler retornar
result, err := db.Query(ctx, "SELECT * FROM users")
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "request timed out", http.StatusGatewayTimeout)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// ... usa o resultado
}É importante lembrar de chamar a função cancel que é retornada por context.WithTimeout, geralmente com defer, para liberar recursos associados ao contexto. Se você não cancelar, o contexto só será cancelado quando o timeout expirar, o que pode causar vazamentos se o handler terminar antes. Além disso, ao usar timeouts, você deve distinguir entre erros de timeout (context.DeadlineExceeded) e erros de cancelamento (context.Canceled) para responder adequadamente ao cliente.
Boas práticas
Ao trabalhar com context em handlers web, siga estas boas práticas:
- Sempre propague o context da requisição (
r.Context()) para todas as operações que podem ser canceladas ou que dependem da requisição. - Nunca armazene context em structs ou variáveis globais; ele deve ser passado explicitamente como argumento.
- Use
context.WithValuecom moderação e apenas para dados que são verdadeiramente necessários em toda a cadeia. - Defina timeouts para operações de I/O e chamadas externas, usando
context.WithTimeoutoucontext.WithDeadline. - Sempre chame a função
cancelretornada quando você criar um context cancelável ou com timeout. - Verifique erros de contexto (
context.Canceled,context.DeadlineExceeded) e trate-os de forma adequada, evitando escrever respostas após o cancelamento.
Exercícios
- Escreva um handler que use o context da requisição para executar uma operação que pode ser cancelada. Simule um cancelamento chamando
cancelapós 1 segundo e observe o comportamento. - Crie um middleware que adicione um ID de requisição ao context e um handler que o recupere e o retorne na resposta.
- Implemente um handler que use
context.WithTimeoutpara limitar uma chamada a uma API externa a 100ms. Se o timeout expirar, retorne um erro 504. - Explique por que é importante propagar o context da requisição em vez de usar
context.Background(). - Escreva um handler que use
context.WithValuepara passar um valor de autenticação e um middleware que o valide.
func handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
// Simula uma operação que pode ser cancelada
go func() {
time.Sleep(2 * time.Second)
cancel() // cancela após 2 segundos
}()
select {
case <-ctx.Done():
http.Error(w, "operation canceled", http.StatusRequestTimeout)
case <-time.After(3 * time.Second):
fmt.Fprintln(w, "operation completed")
}
}type contextKey string
const requestIDKey contextKey = "requestID"
func requestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := uuid.New().String() // use um gerador de UUID
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func handler(w http.ResponseWriter, r *http.Request) {
requestID, ok := r.Context().Value(requestIDKey).(string)
if !ok {
http.Error(w, "request ID not found", http.StatusInternalServerError)
return
}
w.Header().Set("X-Request-ID", requestID)
fmt.Fprintln(w, "Hello")
}func handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 100*time.Millisecond)
defer cancel()
// Simula uma chamada externa
result, err := callExternalAPI(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "external API timeout", http.StatusGatewayTimeout)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintln(w, result)
}
func callExternalAPI(ctx context.Context) (string, error) {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(200 * time.Millisecond):
return "ok", nil
}
}context.Background() não tem cancelamento nem prazos, então operações podem ficar presas indefinidamente.type contextKey string
const authTokenKey contextKey = "authToken"
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "missing token", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), authTokenKey, token)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func handler(w http.ResponseWriter, r *http.Request) {
token, ok := r.Context().Value(authTokenKey).(string)
if !ok {
http.Error(w, "token not found", http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "Token: %s", token)
}