
Em Go, poucos pacotes são tão fundamentais e ao mesmo tempo tão mal utilizados quanto context.Context. Ele é a ferramenta padrão para carregar deadlines, sinais de cancelamento e valores compartilhados ao longo da cadeia de chamadas de uma API. Quando bem usado, evita vazamento de goroutines, implementa timeouts elegantemente e torna seu código robusto. Quando mal usado… bem, você já deve ter visto context.Background() dentro de uma função que deveria honrar o cancelamento.
Neste post, vamos explorar o pacote context em profundidade: como criar contextos com timeout/deadline, como cancelar manualmente, como propagar valores (com cuidado) e como evitar as armadilhas mais comuns. Tudo com exemplos práticos para aplicações HTTP, gRPC, banco de dados e filas.
O que é context.Context?
A interface context.Context tem quatro métodos:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
- Deadline: retorna o momento em que o contexto será cancelado (se houver).
- Done: canal fechado quando o contexto é cancelado ou atinge o deadline.
- Err: retorna o motivo do cancelamento (
CanceledouDeadlineExceeded). - Value: retorna um valor associado a uma chave (uso muito específico).
Contextos são imutáveis. Para criar um contexto com timeout, você deriva um novo a partir de um pai.
Criando contextos: as funções da biblioteca padrão
| Função | Retorno | Uso |
|---|---|---|
context.Background() | Contexto vazio, nunca cancelado | Raiz da árvore, geralmente em main ou testes |
context.TODO() | Similar ao Background | Quando você ainda não sabe qual contexto usar |
context.WithCancel(parent) | (ctx, cancel) | Cancelamento manual |
context.WithTimeout(parent, timeout) | (ctx, cancel) | Cancelamento automático após tempo |
context.WithDeadline(parent, time) | (ctx, cancel) | Cancelamento em momento absoluto |
context.WithValue(parent, key, val) | ctx | Propaga valores (use com parcimônia) |
Todas as funções que retornam um cancelador (cancel func()) devem ser chamadas eventualmente para liberar recursos. Use defer cancel() logo após a criação.
Exemplo 1: Timeout em requisição HTTP
O caso de uso mais comum: você não quer que uma chamada HTTP demore para sempre.
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func fetchUser(ctx context.Context, id string) (*User, error) {
// Cria contexto com timeout de 2 segundos
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel() // importante!
req, err := http.NewRequestWithContext(ctx, "GET", "http://api.example.com/users/"+id, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
// Se o erro foi por timeout, ctx.Err() será DeadlineExceeded
return nil, err
}
defer resp.Body.Close()
// processar resposta...
}
func main() {
ctx := context.Background()
user, err := fetchUser(ctx, "123")
if err != nil {
fmt.Println("Erro:", err)
}
}
Se a chamada HTTP demorar mais de 2 segundos, o contexto é cancelado e a requisição é abortada. O defer cancel() garante que o contexto seja liberado.
Exemplo 2: Cancelamento manual para operações concorrentes
Às vezes você quer cancelar uma operação quando outra termina primeiro (ex: busca paralela em duas fontes).
func searchFastest(ctx context.Context, query string) string {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
result := make(chan string, 2)
// Busca em duas fontes simultaneamente
go func() {
result <- searchGoogle(ctx, query)
}()
go func() {
result <- searchBing(ctx, query)
}()
// Quem responder primeiro vence; o cancel cancela a outra goroutine
select {
case res := <-result:
cancel() // opcional, mas bom para liberar mais rápido
return res
case <-ctx.Done():
return ""
}
}
Assim que um resultado chegar, cancelamos o contexto, e a outra goroutine (se honrar o contexto) deve interromper seu processamento.
Exemplo 3: Propagando contexto para banco de dados
A maioria dos drivers em Go aceita contexto: db.QueryContext(ctx, ...), db.ExecContext(ctx, ...).
func getOrder(ctx context.Context, db *sql.DB, orderID string) (*Order, error) {
// Limita a consulta a 1 segundo
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
row := db.QueryRowContext(ctx, "SELECT id, total FROM orders WHERE id = $1", orderID)
var order Order
if err := row.Scan(&order.ID, &order.Total); err != nil {
return nil, err
}
return &order, nil
}
Se a query demorar mais que 1 segundo, QueryRowContext retorna um erro (geralmente context deadline exceeded).
Exemplo 4: Contexto em servidor HTTP (já é embutido)
A partir do Go 1.7, http.Request já tem um contexto: r.Context(). Use-o para propagar timeouts e cancelamentos.
func slowHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // será cancelado se o cliente desconectar ou timeouts do servidor
select {
case <-time.After(10 * time.Second):
w.Write([]byte("Done"))
case <-ctx.Done():
log.Println("Cliente cancelou a requisição")
w.WriteHeader(http.StatusServiceUnavailable)
}
}
Valor no contexto: use com moderação
context.WithValue permite carregar valores (como request IDs, usuário autenticado, etc.) na árvore de chamadas.
Recomendações oficiais:
- Use apenas para dados de escopo de requisição (ex: trace ID, user info, deadline).
- Não use para parâmetros opcionais de função.
- As chaves devem ser de tipo próprio (ex:
type contextKey string), não tipos primitivos (para evitar colisão).
type contextKey string
const userIDKey contextKey = "userID"
func SetUserID(ctx context.Context, userID string) context.Context {
return context.WithValue(ctx, userIDKey, userID)
}
func GetUserID(ctx context.Context) (string, bool) {
val := ctx.Value(userIDKey)
if val == nil {
return "", false
}
userID, ok := val.(string)
return userID, ok
}
Nunca coloque dados mutáveis (ponteiros que serão modificados) no contexto – isso pode causar data races.
Armadilhas comuns e como evitá-las
1. Esquecer de chamar cancel
Se você cria um contexto com WithCancel, WithTimeout ou WithDeadline e não chama cancel, o contexto vaza recursos (a goroutine interna do contexto fica retida até o cancelamento automático). Sempre use defer cancel().
ctx, cancel := context.WithTimeout(parent, time.Second)
defer cancel() // absolutamente necessário
2. Usar context.Background() onde deveria receber um contexto
Funções que fazem I/O devem aceitar ctx context.Context como primeiro parâmetro. Isso permite que quem chama controle timeouts.
// Ruim
func GetUser(id string) (*User, error)
// Bom
func GetUser(ctx context.Context, id string) (*User, error)
3. Ignorar o erro de cancelamento
Ao chamar funções que aceitam contexto, verifique se o erro foi causado por cancelamento ou timeout.
err := someFunc(ctx)
if errors.Is(err, context.Canceled) {
// cancelamento intencional (ex: usuário abortou)
} else if errors.Is(err, context.DeadlineExceeded) {
// timeout
}
4. Propagando contextos que nunca cancelam para operações longas
Se você tem um contexto raiz (ex: de uma goroutine que vive para sempre), não use context.Background() diretamente para operações que devem ser canceladas. Prefira criar um contexto filho com timeout.
5. Misturar contexto e goroutines sem select no Done
Quando você dispara uma goroutine que deve respeitar cancelamento, sempre use select no canal Done.
go func() {
for {
select {
case <-ctx.Done():
return
default:
// faz trabalho
}
}
}()
Testando código com context
Simule timeouts e cancelamentos em testes:
func TestFetchUser_Timeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
defer cancel()
_, err := fetchUser(ctx, "123")
if err == nil {
t.Error("esperava erro de timeout, mas não ocorreu")
}
}
Conclusão
context.Context é uma das ferramentas mais poderosas e idiomáticas de Go. Ele padroniza a forma de lidar com cancelamento, deadlines e valores de escopo de requisição. Domine os padrões: aceite contexto como primeiro parâmetro, use defer cancel() para evitar vazamentos, honre ctx.Done() em goroutines e nunca abuse de WithValue.
Na Jacobus Software, revisamos constantemente o uso de contextos nos códigos de nossos clientes. Um contexto bem aplicado é a diferença entre um sistema que respira e um que engasga sob carga.
⏱️ Seu código Go ignora contextos? Vamos resolver.
Nossos especialistas revisam e refatoram o uso de contextos em sua base de código – evitando vazamentos de goroutine e garantindo timeouts consistentes.
