Imagine um servidor HTTP recebendo uma requisição que desencadeia três operações: uma consulta ao banco de dados, uma chamada a uma API externa e uma busca em cache. Cada operação acontece em sua própria goroutine. No meio do caminho, o cliente cancela a requisição — fecha a conexão ou atinge o timeout do lado dele.
Sem um mecanismo de propagação, as três operações continuariam executando, consumindo recursos, fazendo queries e chamadas de rede para produzir um resultado que nunca será usado. Em um servidor sob carga, esse desperdício acumula e degrada toda a aplicação.
O pacote context resolve exatamente esse problema. Ele fornece um objeto que carrega um sinal de cancelamento, um prazo e valores arbitrários, e pode ser propagado por toda a árvore de chamadas de uma requisição — através de fronteiras de goroutines, chamadas HTTP, consultas a banco de dados e qualquer outra operação que aceite um contexto.
A interface Context
O contexto é definido por uma interface simples de quatro métodos:
type Context interface {
// Retorna o prazo final, se houver
Deadline() (deadline time.Time, ok bool)
// Channel fechado quando o contexto é cancelado
Done() <-chan struct{}
// Motivo do cancelamento — nil se ainda não cancelado
Err() error
// Valor associado a uma chave — para dados de requisição
Value(key any) any
}
Quatro implementações concretas formam a base do sistema: context.Background(), context.TODO(), e os contextos derivados criados por WithCancel, WithDeadline, WithTimeout e WithValue.
Os contextos raiz
Todo contexto começa a partir de um dos dois contextos raiz:
// Background — contexto raiz para a aplicação inteira
// Nunca é cancelado, não tem deadline, não tem valores
ctx := context.Background()
// TODO — placeholder para código onde o contexto ainda não foi definido
// Semanticamente indica "preciso adicionar context aqui depois"
ctx := context.TODO()
context.Background() é o ponto de partida em main, em testes e em goroutines de nível superior. context.TODO() é uma marcação intencional para código em transição — o analisador estático golangci-lint pode identificá-los e lembrar o desenvolvedor de substituí-los.
WithCancel: cancelamento manual
package main
import (
"context"
"fmt"
"time"
)
func monitorarSistema(ctx context.Context, nome string) {
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Printf("[%s] sistema operacional\n", nome)
case <-ctx.Done():
fmt.Printf("[%s] monitoramento encerrado: %v\n", nome, ctx.Err())
return
}
}
}
func main() {
ctx, cancelar := context.WithCancel(context.Background())
go monitorarSistema(ctx, "CPU")
go monitorarSistema(ctx, "Memória")
go monitorarSistema(ctx, "Disco")
time.Sleep(600 * time.Millisecond)
fmt.Println("cancelando todos os monitores...")
cancelar() // propaga para todas as goroutines simultaneamente
time.Sleep(100 * time.Millisecond)
fmt.Println("encerrado")
}
cancelar() fecha o channel Done() do contexto e de todos os seus filhos simultaneamente. Todas as goroutines que aguardam <-ctx.Done() desbloqueiam ao mesmo tempo — um broadcast eficiente sem nenhuma comunicação adicional.
WithTimeout e WithDeadline
WithTimeout recebe uma duração relativa ao momento atual. WithDeadline recebe um instante absoluto no tempo. Ambos cancelam automaticamente o contexto quando o prazo é atingido:
package main
import (
"context"
"fmt"
"time"
)
func buscarDados(ctx context.Context, fonte string, latencia time.Duration) (string, error) {
select {
case <-time.After(latencia):
return fmt.Sprintf("dados de %s", fonte), nil
case <-ctx.Done():
return "", fmt.Errorf("%s: %w", fonte, ctx.Err())
}
}
func main() {
// WithTimeout — deadline relativo
ctx, cancelar := context.WithTimeout(context.Background(), 300*time.Millisecond)
defer cancelar()
dados, err := buscarDados(ctx, "banco_primario", 200*time.Millisecond)
if err != nil {
fmt.Println("Erro:", err)
} else {
fmt.Println("Sucesso:", dados)
}
// WithDeadline — deadline absoluto
prazo := time.Now().Add(100 * time.Millisecond)
ctxDL, cancelDL := context.WithDeadline(context.Background(), prazo)
defer cancelDL()
dados, err = buscarDados(ctxDL, "banco_secundario", 300*time.Millisecond)
if err != nil {
fmt.Println("Erro:", err) // context deadline exceeded
} else {
fmt.Println("Sucesso:", dados)
}
// Verificando o deadline
if dl, ok := ctxDL.Deadline(); ok {
fmt.Printf("Deadline foi: %s\n", dl.Format("15:04:05.000"))
}
}
A árvore de contextos
Contextos formam uma árvore hierárquica. Cancelar um contexto pai cancela todos os filhos — mas cancelar um filho não afeta o pai nem os irmãos:
package main
import (
"context"
"fmt"
"time"
)
func executar(ctx context.Context, nome string, duracao time.Duration) {
select {
case <-time.After(duracao):
fmt.Printf("[%s] concluído normalmente\n", nome)
case <-ctx.Done():
fmt.Printf("[%s] cancelado: %v\n", nome, ctx.Err())
}
}
func main() {
// Raiz
raiz, cancelarRaiz := context.WithCancel(context.Background())
defer cancelarRaiz()
// Filho A — timeout de 400ms
filhoA, cancelarA := context.WithTimeout(raiz, 400*time.Millisecond)
defer cancelarA()
// Filho B — timeout de 800ms
filhoB, cancelarB := context.WithTimeout(raiz, 800*time.Millisecond)
defer cancelarB()
// Neto de A — timeout de 200ms
neto, cancelarNeto := context.WithTimeout(filhoA, 200*time.Millisecond)
defer cancelarNeto()
go executar(filhoA, "filho-A", 600*time.Millisecond) // será cancelado por filhoA
go executar(filhoB, "filho-B", 600*time.Millisecond) // concluirá normalmente
go executar(neto, "neto", 600*time.Millisecond) // será cancelado por neto (200ms)
time.Sleep(1 * time.Second)
// neto cancela em 200ms
// filhoA cancela em 400ms
// filhoB conclui normalmente em 600ms (antes dos 800ms)
}
WithValue: transportando dados de requisição
context.WithValue associa um par chave-valor ao contexto. Valores propagam pela árvore — filhos herdam os valores dos pais:
package main
import (
"context"
"fmt"
)
// Tipos próprios para chaves — evita colisões com outros pacotes
type chaveContexto string
const (
ChaveRequisicaoID chaveContexto = "requisicao_id"
ChaveUsuarioID chaveContexto = "usuario_id"
ChavePapel chaveContexto = "papel"
)
func middleware(ctx context.Context, reqID, userID, papel string) context.Context {
ctx = context.WithValue(ctx, ChaveRequisicaoID, reqID)
ctx = context.WithValue(ctx, ChaveUsuarioID, userID)
ctx = context.WithValue(ctx, ChavePapel, papel)
return ctx
}
func obterRequisicaoID(ctx context.Context) string {
v, _ := ctx.Value(ChaveRequisicaoID).(string)
return v
}
func obterUsuarioID(ctx context.Context) string {
v, _ := ctx.Value(ChaveUsuarioID).(string)
return v
}
func processarPedido(ctx context.Context, pedidoID int) {
reqID := obterRequisicaoID(ctx)
userID := obterUsuarioID(ctx)
papel, _ := ctx.Value(ChavePapel).(string)
fmt.Printf("[req:%s] usuário %s (papel:%s) processando pedido %d\n",
reqID, userID, papel, pedidoID)
}
func main() {
ctx := context.Background()
ctx = middleware(ctx, "req-abc-123", "user-42", "admin")
processarPedido(ctx, 1001)
processarPedido(ctx, 1002)
}
Regras importantes para WithValue:
Use tipos próprios como chave — nunca strings ou ints diretamente — para evitar colisões entre pacotes diferentes. Use WithValue apenas para dados de escopo de requisição: IDs de rastreamento, tokens de autenticação, metadados de telemetria. Nunca use para passar parâmetros opcionais de funções — isso torna as dependências implícitas e dificulta testes.
context em handlers HTTP
O caso de uso mais comum em produção. O net/http injeta automaticamente um contexto em cada requisição, que é cancelado quando a conexão do cliente se encerra:
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
type chaveHTTP string
const ChaveRequestID chaveHTTP = "request_id"
// Middleware que injeta request ID e timeout
func comContexto(prox http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Timeout de 5 segundos por requisição
ctx, cancelar := context.WithTimeout(r.Context(), 5*time.Second)
defer cancelar()
// Injeta request ID
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
reqID = fmt.Sprintf("req-%d", time.Now().UnixNano())
}
ctx = context.WithValue(ctx, ChaveRequestID, reqID)
prox.ServeHTTP(w, r.WithContext(ctx))
})
}
func consultarBancoDados(ctx context.Context, query string) (map[string]any, error) {
// Simula consulta que respeita o contexto
select {
case <-time.After(200 * time.Millisecond):
return map[string]any{"query": query, "resultado": "dados"}, nil
case <-ctx.Done():
return nil, fmt.Errorf("consulta cancelada: %w", ctx.Err())
}
}
func handlerUsuario(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
reqID, _ := ctx.Value(ChaveRequestID).(string)
dados, err := consultarBancoDados(ctx, "SELECT * FROM usuarios")
if err != nil {
log.Printf("[%s] erro: %v", reqID, err)
http.Error(w, "erro interno", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Request-ID", reqID)
json.NewEncoder(w).Encode(dados)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/usuarios", handlerUsuario)
handler := comContexto(mux)
servidor := &http.Server{
Addr: ":8080",
Handler: handler,
}
log.Println("servidor em :8080")
log.Fatal(servidor.ListenAndServe())
}
context com banco de dados
A biblioteca database/sql aceita context em todos os métodos de consulta, permitindo cancelar queries em andamento:
package main
import (
"context"
"database/sql"
"fmt"
"time"
_ "github.com/lib/pq"
)
type RepositorioUsuario struct {
db *sql.DB
}
func (r *RepositorioUsuario) BuscarPorID(ctx context.Context, id int) error {
// Context propaga para o driver — cancela a query no banco se necessário
row := r.db.QueryRowContext(ctx,
"SELECT id, nome, email FROM usuarios WHERE id = $1", id)
var (
userID int
nome string
email string
)
if err := row.Scan(&userID, &nome, &email); err != nil {
return fmt.Errorf("BuscarPorID: %w", err)
}
fmt.Printf("usuário: %d %s <%s>\n", userID, nome, email)
return nil
}
func (r *RepositorioUsuario) ListarAtivos(ctx context.Context) error {
rows, err := r.db.QueryContext(ctx,
"SELECT id, nome FROM usuarios WHERE ativo = true ORDER BY nome")
if err != nil {
return fmt.Errorf("ListarAtivos: %w", err)
}
defer rows.Close()
for rows.Next() {
// Verifica cancelamento entre linhas
select {
case <-ctx.Done():
return fmt.Errorf("ListarAtivos cancelado: %w", ctx.Err())
default:
}
var id int
var nome string
if err := rows.Scan(&id, &nome); err != nil {
return err
}
fmt.Printf("%d: %s\n", id, nome)
}
return rows.Err()
}
func exemplo() {
// Exemplo de uso com timeout
ctx, cancelar := context.WithTimeout(context.Background(), 3*time.Second)
defer cancelar()
repo := &RepositorioUsuario{ /* db inicializado */ }
if err := repo.BuscarPorID(ctx, 42); err != nil {
fmt.Println("erro:", err)
}
}
Boas práticas consolidadas
Context deve ser o primeiro parâmetro, sempre nomeado ctx:
// Correto
func ProcessarPedido(ctx context.Context, pedidoID int) error
// Errado
func ProcessarPedido(pedidoID int, ctx context.Context) error
func ProcessarPedido(c context.Context, pedidoID int) error
Nunca armazene context em structs. Context deve fluir pela pilha de chamadas como parâmetro de função, não ser armazenado e reutilizado:
// Errado
type Servico struct {
ctx context.Context
}
// Correto — ctx como parâmetro de método
func (s *Servico) Executar(ctx context.Context) error
Sempre chame cancelar. Mesmo que o contexto já tenha sido cancelado:
ctx, cancelar := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelar() // sempre, mesmo se cancelado antes
Propague context, nunca crie novos a partir de Background em funções internas:
// Errado — cria contexto independente, ignora cancelamento do chamador
func buscar(id int) {
ctx := context.Background() // perde o contexto da requisição
db.QueryContext(ctx, ...)
}
// Correto — propaga o contexto recebido
func buscar(ctx context.Context, id int) {
db.QueryContext(ctx, ...)
}
Resumo do que foi coberto
Este artigo apresentou o pacote context em profundidade: a interface Context e seus quatro métodos, os contextos raiz Background e TODO, criação de contextos derivados com WithCancel, WithTimeout e WithDeadline, a árvore de contextos e propagação de cancelamento, transporte de valores com WithValue, integração com handlers HTTP e banco de dados, e as boas práticas consolidadas pela comunidade. O próximo artigo explora padrões avançados de concorrência.
Referências e leituras complementares
-
Documentação do pacote context — Referência completa e exemplos oficiais. https://pkg.go.dev/context
-
Go Blog: Go Concurrency Patterns: Context — Artigo original introduzindo o pacote context. https://go.dev/blog/context
-
Go Blog: Contexts and structs — Por que context não deve ser armazenado em structs. https://go.dev/blog/context-and-structs
-
Go by Example: Context — Exemplos práticos e comentados. https://gobyexample.com/context
-
Documentação database/sql — QueryContext — Referência de métodos que aceitam context. https://pkg.go.dev/database/sql#DB.QueryContext
-
OpenTelemetry Go — Como context é usado para propagação de traces em sistemas distribuídos. https://opentelemetry.io/docs/instrumentation/go/