Contexto Context do Go feito da forma correta: cancelamento, timeouts e valores
O context do Go é fluxo de controle, não armazenamento.
O context.Context do Go é simples o suficiente para ser mal utilizado — e esse é o problema.
A maioria dos desenvolvedores Go aprende as regras superficiais rapidamente: passar context como primeiro argumento, verificar ctx.Done(), usar context.WithTimeout e nunca passar nil.
func DoSomething(ctx context.Context) error {
// ...
}
Essas regras são úteis, mas cobrem a parte fácil. Em serviços de produção, context não é apenas uma convenção de parâmetro — é o plano de controle para o tempo de vida da requisição.

Contexto informa ao trabalho quando parar, quanto tempo resta, qual caminho de cancelamento foi tomado e quais valores com escopo de requisição precisam atravessar fronteiras de API. Usado bem, ele previne vazamentos de goroutines, evita trabalho desperdiçado, propaga prazos e facilita o encerramento de serviços. Usado mal, ele se torna um saco de dependências ocultas, globais falsas, timeouts esquecidos, timers vazados e comportamento de cancelamento confuso.
A versão um pouco opinativa é esta: use context para cancelamento, prazos e metadados com escopo de requisição, e não o use como um contêiner de dependências.
Para que serve o context
O pacote context tem três principais funções — cancelamento, prazos e timeouts, e valores com escopo de requisição — e essas três funções cobrem tudo para o que ele foi projetado.
Um context deve responder a perguntas como:
Esta requisição foi cancelada?
Quanto tempo esta operação tem restante?
Qual ID de requisição deve ser anexado aos logs?
Qual usuário autenticado está associado a esta requisição?
Um context não deve responder a perguntas como:
Onde está minha conexão com o banco de dados?
Onde está meu logger?
Onde está minha configuração?
Qual implementação de serviço devo usar?
Essas são dependências — passe-as explicitamente através de parâmetros de função (veja Injeção de Dependência em Go para padrões de como fazer isso de forma limpa). Contexto é para tempo de vida da requisição e metadados da requisição, não para fiação da aplicação.
A forma básica do context
A interface principal é pequena:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
As partes importantes são:
Done()é fechado quando o contexto é cancelado ou seu prazo expira.Err()explica por que o contexto terminou.Deadline()informa se o contexto tem um prazo.Value()armazena dados com escopo de requisição.
A maioria do código não implementa esta interface. Ele recebe um contexto e o passa adiante.
A primeira regra: passe context explicitamente
Para funções que realizam trabalho com escopo de requisição ou cancelável, passe context como o primeiro parâmetro — esta é a convenção padrão do Go e o que todas as bibliotecas e ferramentas do ecossistema esperam:
func GetUser(ctx context.Context, id string) (*User, error) {
// ...
}
Faça isso para funções que possam:
- Chamar um banco de dados
- Chamar outro serviço
- Esperar por uma fila
- Iniciar trabalho em segundo plano
- Bloquear em E/S
- Usar um timeout
- Necessitar valores com escopo de requisição
- Necessitar cancelamento
Não adicione context a pequenas funções puras que não precisam dele.
Isto é aceitável:
func NormalizeEmail(email string) string {
return strings.ToLower(strings.TrimSpace(email))
}
Nem toda função precisa de um context. Adicionar context em todos os lugares torna o código barulhento.
Não armazene context em structs
Armazenar um context em um struct é um dos erros mais comuns em bases de código Go, e vale a pena destacá-lo explicitamente. Não faça isto:
type UserService struct {
ctx context.Context
db *sql.DB
}
Faça isto em vez disso:
type UserService struct {
db *sql.DB
}
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
// ...
}
Um context pertence a uma requisição, operação ou tarefa, enquanto um struct de serviço geralmente vive muito mais tempo do que qualquer requisição individual. Misturar essas durações torna o cancelamento pouco claro e dificulta raciocinar sobre qual operação um contexto pertence.
Existem raras exceções para tipos que genuinamente representam uma duração única de operação, mas são raras o suficiente para que a regra padrão deva ser simples:
Passe context. Não o armazene.
Não passe context nil
Nunca passe nil como contexto.
Ruim:
err := svc.DoWork(nil)
Use context.Background() quando não houver um contexto existente:
err := svc.DoWork(context.Background())
Em testes, use o contexto de teste quando possível:
func TestDoWork(t *testing.T) {
err := svc.DoWork(t.Context())
if err != nil {
t.Fatal(err)
}
}
Um contexto nil pode causar panic quando o código chama métodos nele. Um contexto de background é explícito e seguro.
Contextos de Background, TODO e de requisição
Há três pontos de partida comuns.
context.Background
Use context.Background() no nível superior de um programa quando não existir um contexto pai — é o contexto raiz do qual todos os contextos filhos são derivados:
func main() {
ctx := context.Background()
_ = run(ctx)
}
ou:
func TestSomething(t *testing.T) {
ctx := context.Background()
_ = ctx
}
context.TODO
Use context.TODO() quando você sabe que um contexto deve ser usado, mas ainda não decidiu qual.
ctx := context.TODO()
Isto é útil durante migração, mas não deve se tornar permanente se um contexto real existir.
Contexto de requisição
Em servidores HTTP, use o contexto da requisição:
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
_ = ctx
}
O contexto da requisição é cancelado quando a conexão do cliente é fechada, a requisição é cancelada ou o servidor termina o processamento da requisição.
Para serviços web, este é geralmente o contexto que você deve passar para o código da aplicação.
Cancelamento com context.WithCancel
Use context.WithCancel quando você deseja parar o trabalho explicitamente.
ctx, cancel := context.WithCancel(parent)
defer cancel()
A função cancel retornada cancela o contexto filho e libera os recursos associados a ele. Sempre chame-a quando terminar — mesmo que o contexto eventualmente atinja o timeout, chamar cancel antecipadamente evita manter recursos ativos por mais tempo do que o necessário.
Exemplo:
func RunWorker(parent context.Context) error {
ctx, cancel := context.WithCancel(parent)
defer cancel()
done := make(chan error, 1)
go func() {
done <- doBackgroundWork(ctx)
}()
select {
case <-ctx.Done():
return ctx.Err()
case err := <-done:
return err
}
}
O padrão é simples:
- Derive um contexto filho.
- Defer cancel.
- Passe o contexto filho para trabalhos que devem parar juntos.
- Observe
ctx.Done().
Timeouts com context.WithTimeout
Use context.WithTimeout quando uma operação tem uma duração máxima.
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
Exemplo com um cliente HTTP:
func FetchUser(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
return client.Do(req)
}
Isso torna o timeout parte da operação, não uma configuração global oculta.
Sempre chame cancel
Quando você chama WithCancel, WithTimeout ou WithDeadline, sempre chame a função cancel retornada — isso importa para a correção.
Bom:
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
defer cancel()
Ruim:
ctx, _ := context.WithTimeout(parent, 5*time.Second)
Falhar em chamar cancel pode manter timers e contextos filhos ativos por mais tempo do que o necessário.
Prazos vs timeouts
Um timeout é relativo:
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
Um prazo é absoluto:
deadline := time.Now().Add(2 * time.Second)
ctx, cancel := context.WithDeadline(parent, deadline)
defer cancel()
A maioria do código da aplicação usa timeouts. Prazos são úteis quando uma requisição tem um tempo final fixo que deve ser compartilhado entre várias operações — por exemplo, se uma requisição tem 900 milissegundos restantes, não dê a cada chamada downstream um novo timeout de 1 segundo; propague o orçamento restante em vez disso.
Orçamentos de timeout entre camadas de serviço
Um erro comum é empilhar timeouts cegamente.
func Handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
_ = service.DoWork(ctx)
}
func (s *Service) DoWork(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
return s.repo.Query(ctx)
}
Isto parece inofensivo, mas oculta o orçamento real. A camada de serviço geralmente deve respeitar o prazo do chamador, em vez de redefinir o timer para o mesmo valor.
Um padrão melhor é:
func Handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
if err := service.DoWork(ctx); err != nil {
// tratar erro
return
}
}
E então dentro do serviço:
func (s *Service) DoWork(ctx context.Context) error {
return s.repo.Query(ctx)
}
Adicione um timeout filho apenas quando uma sub-operação necessitar de um orçamento menor:
func (s *Service) DoWork(ctx context.Context) error {
queryCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer cancel()
return s.repo.Query(queryCtx)
}
O modelo mental correto é direto: toda requisição tem um orçamento externo único, sub-operações específicas podem ter orçamentos menores destacados desse orçamento, e nenhuma camada estende silenciosamente a requisição além do que o chamador pretendia.
Verifique ctx.Err() para distinguir cancelamento de timeout
Quando um contexto termina, ctx.Err() retorna o motivo.
Geralmente é um dos:
context.Canceled
context.DeadlineExceeded
Exemplo:
select {
case <-ctx.Done():
return ctx.Err()
case result := <-resultCh:
return handle(result)
}
Isto permite que os chamadores distingam cancelamento de timeout, e essa distinção importa na prática. Uma requisição cancelada muitas vezes significa que o cliente se desconectou, enquanto um erro de prazo excedido geralmente significa que seu serviço estava lento demais — eles não devem sempre ser registrados, repetidos ou reportados da mesma forma.
Use context.Cause para melhores razões de cancelamento
O Go moderno também suporta cancelamento com conhecimento da causa.
As funções úteis incluem:
context.WithCancelCausecontext.WithTimeoutCausecontext.WithDeadlineCausecontext.Cause
O simples ctx.Err() informa o motivo amplo: cancelado ou prazo excedido.
context.Cause(ctx) pode informar a causa mais específica.
Exemplo:
var ErrShutdown = errors.New("servidor encerrando")
func Run(ctx context.Context) error {
ctx, cancel := context.WithCancelCause(ctx)
defer cancel(nil)
go func() {
// Algum sinal de encerramento chegou.
cancel(ErrShutdown)
}()
<-ctx.Done()
return context.Cause(ctx)
}
Use o cancelamento com conhecimento da causa quando o motivo importa para chamadores, logs ou comportamento de limpeza, e evite-o onde um simples ctx.Err() é suficiente — o detalhe extra só vale a pena quando o diagnóstico genuinamente o requer.
Exemplo de servidor HTTP
Um handler HTTP normal deve começar a partir de r.Context(). Para uma exploração completa de como estruturar serviços HTTP em Go, veja Construindo APIs REST em Go. Gin, Echo e Fiber cada um envolvem context.Context dentro do seu próprio tipo de contexto de requisição, e as diferenças entre as pilhas são comparadas em Frameworks Web Go em 2026: net/http, chi, Gin, Echo, Fiber Comparados.
func GetUserHandler(svc *UserService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := r.PathValue("id")
user, err := svc.GetUser(ctx, id)
if err != nil {
writeError(w, err)
return
}
writeJSON(w, http.StatusOK, user)
}
}
O serviço deve aceitar e propagar o contexto:
type UserService struct {
repo *UserRepository
}
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
return s.repo.GetUser(ctx, id)
}
O repositório deve usar métodos de banco de dados cientes do contexto:
type UserRepository struct {
db *sql.DB
}
func (r *UserRepository) GetUser(ctx context.Context, id string) (*User, error) {
const query = `
select id, email, name
from users
where id = $1
`
var user User
err := r.db.QueryRowContext(ctx, query, id).Scan(
&user.ID,
&user.Email,
&user.Name,
)
if err != nil {
return nil, err
}
return &user, nil
}
A coisa importante é a cadeia — cada camada passa o mesmo contexto para a próxima:
Não quebre a cadeia criando context.Background() no meio.
O erro de context.Background(): quebrando a cadeia de cancelamento
Este é um bug comum:
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
return s.repo.GetUser(context.Background(), id)
}
Isto descarta toda a informação de cancelamento e prazo do chamador. Se o cliente se desconectar, a consulta ao banco de dados continua executando. Se a requisição estourar o timeout, o trabalho downstream pode ainda estar em andamento. Se o servidor estiver encerrando, este código o ignora por completo. Substituir o contexto recebido por context.Background() dentro da lógica de negócio está quase sempre errado.
Use o contexto que você foi dado:
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
return s.repo.GetUser(ctx, id)
}
Use context.Background() apenas na borda onde não existe um contexto pai.
Exemplo de cliente HTTP
Para requisições HTTP de saída, anexe o contexto à requisição.
func CallAPI(ctx context.Context, client *http.Client, endpoint string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
return client.Do(req)
}
Não faça isto:
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
Isso cria uma requisição sem o contexto da operação.
Evite também confiar apenas em http.Client.Timeout. Pode ser útil como um limite de segurança, mas os contextos de requisição dão-lhe melhor propagação através da cadeia de chamadas.
Um padrão comum é:
func CallAPI(ctx context.Context, client *http.Client, endpoint string) (*http.Response, error) {
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
return client.Do(req)
}
Use isto quando a chamada de API downstream tiver um orçamento específico dentro de uma requisição maior.
Exemplo de banco de dados
A maioria das APIs de banco de dados em Go tem métodos cientes do contexto. Para uma visão mais ampla de como as bibliotecas de acesso a dados do Go lidam com context — incluindo GORM, Ent, Bun e sqlc — veja Comparando ORMs Go para PostgreSQL.
Use-os.
Bom:
rows, err := db.QueryContext(ctx, query, args...)
Bom:
err := db.QueryRowContext(ctx, query, id).Scan(&name)
Bom:
result, err := db.ExecContext(ctx, query, args...)
Ruim:
rows, err := db.Query(query, args...)
As formas cientes do contexto permitem que as operações de banco de dados parem quando a requisição é cancelada ou estoura o timeout, o que é especialmente importante para consultas lentas, bancos de dados sobrecarregados e APIs voltadas para o usuário onde a latência afeta diretamente a experiência do usuário.
Transações e context
Transações precisam de um tratamento cuidadoso do contexto.
Uma transação geralmente deve começar com o contexto da operação:
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
E então use o mesmo contexto para operações de transação:
if _, err := tx.ExecContext(ctx, query, args...); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
Tenha cuidado com timeouts em torno de transações. Se o contexto for cancelado antes do Commit, a transação pode ser revertida. Isso pode ser o que você deseja, mas deve ser intencional.
Para transações longas, a melhor resposta geralmente não é um timeout mais longo — é uma transação mais curta que faz menos trabalho por unidade.
Trabalhadores em segundo plano e context
Trabalhadores em segundo plano devem receber um contexto que represente sua duração.
Exemplo:
type Worker struct {
logger *slog.Logger
}
func (w *Worker) Run(ctx context.Context) error {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
if err := w.doOnce(ctx); err != nil {
w.logger.Error("iteração do worker falhou", "err", err)
}
}
}
}
Este worker para limpa quando o contexto é cancelado, e seu ticker é corretamente limpo via defer ticker.Stop(). Em main, você criaria um contexto raiz atrelado a sinais do SO:
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
worker := &Worker{logger: slog.Default()}
if err := worker.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
slog.Error("worker parado", "err", err)
}
}
Isto é contexto usado corretamente: ele descreve a duração do trabalho do processo, e quando o SO envia um sinal, toda a árvore de goroutines que compartilha este contexto parará junta.
Prevenindo vazamentos de goroutines com cancelamento de context
Um vazamento de goroutine acontece quando uma goroutine permanece bloqueada para sempre depois de não ser mais útil.
Contexto ajuda a prevenir isto.
Ruim:
func StartWorker() {
go func() {
for {
doWork()
time.Sleep(time.Second)
}
}()
}
Esta goroutine não tem caminho de desligamento.
Melhor:
func StartWorker(ctx context.Context) {
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
doWork()
}
}
}()
}
Qualquer goroutine que faça loop deve quase sempre ter um caminho de cancelamento.
Isso não significa que toda goroutine deve receber contexto diretamente, mas o sistema deve ter uma maneira clara de pará-la.
context.AfterFunc
context.AfterFunc executa uma função após um contexto ser cancelado.
Pode ser útil para limpeza, desbloquear operações ou fazer a ponte entre APIs que não suportam nativamente contexto.
Exemplo:
func waitWithContext(ctx context.Context, ch <-chan struct{}) error {
stop := context.AfterFunc(ctx, func() {
// Acordar ou limpar se necessário.
})
defer stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-ch:
return nil
}
}
Use AfterFunc com cuidado — ele inicia lógica quando o cancelamento acontece, o que pode tornar o fluxo de controle mais difícil de seguir. Para a maioria do código da aplicação, um select normal em ctx.Done() é mais claro e fácil de raciocinar. AfterFunc é mais valioso quando você precisa adaptar o cancelamento de contexto para uma API que não já aceita contexto.
context.WithoutCancel
context.WithoutCancel cria um contexto que não é cancelado quando o pai é cancelado.
Isto é útil, mas também é fácil de mal usar.
Exemplo de caso de uso:
func Handler(audit *AuditLog) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Processar requisição...
_ = ctx
auditCtx := context.WithoutCancel(ctx)
go func() {
ctx, cancel := context.WithTimeout(auditCtx, 2*time.Second)
defer cancel()
_ = audit.Write(ctx, "requisição concluída")
}()
}
}
A ideia é que a escrita de auditoria pode precisar continuar brevemente mesmo após o contexto da requisição ser cancelado. Isso deve ser raro e deliberado — não use WithoutCancel como uma forma de evitar lidar com cancelamento. Use-o apenas quando o trabalho filho genuinamente deve sobreviver ao cancelamento do pai, e sempre adicione um novo timeout: um contexto que ignora cancelamento, mas não carrega prazo, pode facilmente criar vazamentos de goroutines em segundo plano.
Valores de context feitos corretamente
Valores de contexto são para dados com escopo de requisição que atravessam fronteiras de API.
Exemplos bons:
- ID de requisição
- ID de rastro
- ID de usuário autenticado
- ID de inquilino (tenant)
- idioma
- princípio de segurança
- metadados de correlação
Exemplos ruins:
- conexão com banco de dados
- logger como dependência oculta
- bandeiras de funcionalidade para fluxo de controle ordinário
- parâmetros de função opcionais
- configuração
- clientes de serviço
Uma regra útil: se o valor faz parte da identidade da requisição ou do contexto de observabilidade, ele pode pertencer ao contexto. Se é uma dependência que seu código precisa para fazer seu trabalho, passe-a explicitamente.
Use chaves tipadas para valores de context
Não use strings simples como chaves de contexto.
Ruim:
ctx = context.WithValue(ctx, "userID", "123")
Isto pode colidir com outros pacotes.
Use um tipo de chave customizado não exportado:
type userIDKey struct{}
func WithUserID(ctx context.Context, userID string) context.Context {
return context.WithValue(ctx, userIDKey{}, userID)
}
func UserIDFromContext(ctx context.Context) (string, bool) {
userID, ok := ctx.Value(userIDKey{}).(string)
return userID, ok
}
Este padrão lhe dá segurança de tipo na fronteira do pacote, evita colisões de chaves com outros pacotes e mantém a superfície da API do contexto limpa com funções acessor tipadas.
Não use valores de context para parâmetros opcionais
Isto é ruim:
ctx = context.WithValue(ctx, "pageSize", 100)
users, err := repo.ListUsers(ctx)
Isto oculta o contrato da função.
Prefira parâmetros explícitos:
users, err := repo.ListUsers(ctx, ListUsersOptions{
PageSize: 100,
})
Valores de contexto não devem substituir argumentos de função. Entrada oculta torna o código mais difícil de entender, testar e revisar — e qualquer pessoa lendo a assinatura da função não terá ideia de que o parâmetro existe.
Logging e context
Há duas abordagens comuns para logging com contexto. Os exemplos aqui usam o pacote log/slog do Go — para uma investigação mais aprofundada de logging estruturado com slog em serviços de produção, veja Logging Estruturado em Go com slog.
Abordagem 1: Extraindo valores e anexando-os aos logs
func LogRequest(ctx context.Context, logger *slog.Logger, msg string) {
if requestID, ok := RequestIDFromContext(ctx); ok {
logger = logger.With("request_id", requestID)
}
logger.Info(msg)
}
Isto mantém o logger explícito como uma dependência apropriada e usa contexto apenas para valores com escopo de requisição que genuinamente precisam atravessar fronteiras de API.
Abordagem 2: Armazenar logger em context
Algumas bases de código armazenam um logger em contexto.
Isto pode ser conveniente, mas eu não recomendo como padrão. Transforma o contexto em um contêiner de dependências.
Minha preferência:
- Passe dependências de logger explicitamente.
- Armazene IDs de rastro e IDs de requisição em contexto.
- Adicione esses valores aos logs em fronteiras ou middleware.
Isto mantém as dependências visíveis.
Contexto e rastro (tracing)
Rastro é um dos casos de uso mais fortes para valores de contexto, e é um encaixe genuinamente bom. OpenTelemetry e sistemas similares usam contexto para propagar spans de rastro através de chamadas de função e fronteiras de processo, porque dados de rastro são exatamente o tipo de metadado com escopo de requisição para o qual o contexto foi projetado.
Um padrão típico parece:
func (s *Service) DoWork(ctx context.Context) error {
ctx, span := s.tracer.Start(ctx, "Service.DoWork")
defer span.End()
return s.repo.Query(ctx)
}
O contexto carrega o span de rastro ativo, e o repositório pode criar um span filho a partir dele. Cada camada adiciona seu próprio span sem nenhuma passagem explícita de objetos de tracer — o contexto faz esse trabalho transparentemente através da inteira árvore de chamadas.
Tratamento de erros com context
Quando uma operação para devido a cancelamento de contexto, preserve essa informação. Os padrões aqui complementam as estratégias mais amplas de design de erro cobertas em Arquitetura de Tratamento de Erros em Go.
Exemplo:
err := svc.DoWork(ctx)
if err != nil {
if errors.Is(err, context.Canceled) {
// Cliente cancelou ou chamador parou o trabalho.
return err
}
if errors.Is(err, context.DeadlineExceeded) {
// Timeout.
return err
}
return err
}
Não embrulhe cegamente erros de contexto de uma forma que os oculte.
Wrapping com %w preserva errors.Is, então chamadores ainda podem detectar cancelamento ou timeout:
if err != nil {
return fmt.Errorf("consultar usuário: %w", err)
}
Substituindo o erro totalmente descarta essa informação e quebra qualquer chamador que verifique por tipos específicos de erro de contexto:
if err != nil {
return errors.New("falha ao consultar usuário")
}
Mapeando erros de context para respostas HTTP
Erros de contexto muitas vezes se mapeiam para resultados HTTP diferentes.
Exemplo:
func writeError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, context.Canceled):
// O cliente provavelmente se foi.
// Alguns sistemas registram isso como requisição fechada pelo cliente.
return
case errors.Is(err, context.DeadlineExceeded):
http.Error(w, "requisição estourou o timeout", http.StatusGatewayTimeout)
return
default:
http.Error(w, "erro interno do servidor", http.StatusInternalServerError)
return
}
}
Não trate cancelamento do cliente como falha da aplicação — se o usuário fechou a aba do navegador, isso não é seu serviço se comportando mal, e registrá-lo como erro adiciona ruído sem sinal.
Contexto em middleware
Middleware HTTP é um lugar comum para adicionar valores com escopo de requisição.
Exemplo de middleware de ID de requisição:
type requestIDKey struct{}
func WithRequestID(ctx context.Context, requestID string) context.Context {
return context.WithValue(ctx, requestIDKey{}, requestID)
}
func RequestIDFromContext(ctx context.Context) (string, bool) {
requestID, ok := ctx.Value(requestIDKey{}).(string)
return requestID, ok
}
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = newRequestID()
}
ctx := WithRequestID(r.Context(), requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Isto é um bom uso de contexto. O ID de requisição pertence à requisição, ele deve viajar pela cadeia de chamadas completa, e anexá-lo a logs e rastros em cada camada é exatamente o tipo de preocupação de observabilidade transversal que valores de contexto são projetados para suportar.
Contexto em testes
Em testes, evite usar context.Background() cegamente.
Prefira t.Context() quando o trabalho pertence à duração do teste:
func TestService(t *testing.T) {
ctx := t.Context()
err := service.DoWork(ctx)
if err != nil {
t.Fatal(err)
}
}
Para comportamento de timeout, teste com um timeout real apenas se o timeout for pequeno e significativo.
Para código concorrente e dependente do tempo, considere usar testing/synctest — Testando Código Go Concorrente com synctest cobre esta ferramenta em profundidade:
func TestTimeout(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
defer cancel()
time.Sleep(30 * time.Second)
if !errors.Is(ctx.Err(), context.DeadlineExceeded) {
t.Fatalf("obteve %v, quis deadline exceeded", ctx.Err())
}
})
}
Isto permite testar valores de timeout reais sem esperar pelo tempo real.
Contexto e errgroup
Para grupos de goroutines que devem cancelar juntas, errgroup é muitas vezes um bom encaixe.
Exemplo:
func FetchAll(ctx context.Context, ids []string, client *Client) error {
g, ctx := errgroup.WithContext(ctx)
for _, id := range ids {
id := id
g.Go(func() error {
_, err := client.Fetch(ctx, id)
return err
})
}
return g.Wait()
}
Se uma goroutine retorna um erro, o contexto do grupo é cancelado e outras goroutines que respeitam ctx.Done() podem parar antecipadamente. Isto é muito mais limpo do que gerenciar manualmente múltiplas goroutines, canais e caminhos de cancelamento. A frase-chave aqui é “respeitar o contexto” — errgroup não pode parar trabalho que ignora ctx.Done().
Encerramento gracioso
Contexto é central para o encerramento gracioso.
Uma configuração típica de servidor tem:
- um contexto raiz cancelado por sinais do SO
- um servidor HTTP
- trabalhadores em segundo plano
- um timeout de encerramento
- lógica de limpeza
Exemplo:
func main() {
root, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
server := &http.Server{
Addr: ":8080",
Handler: routes(),
}
go func() {
<-root.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
slog.Error("falha no encerramento do servidor", "err", err)
}
}()
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("servidor falhou", "err", err)
os.Exit(1)
}
}
Note que o contexto de encerramento não é o mesmo que o contexto raiz — o raiz já está cancelado quando o sinal do SO chega. Um contexto de timeout separado dá ao processo de encerramento uma quantidade limitada de tempo para drenar requisições em andamento antes de forçar a saída, que é a distinção sutil, mas importante, que faz o encerramento gracioso funcionar de verdade.
Anti-padrões comuns
Anti-padrão 1: Usando contexto como contêiner de dependências
Ruim:
ctx = context.WithValue(ctx, "db", db)
ctx = context.WithValue(ctx, "logger", logger)
ctx = context.WithValue(ctx, "config", cfg)
Passe dependências explicitamente.
Anti-padrão 2: Criando context.Background dentro da lógica de negócio
Ruim:
func (s *Service) DoWork(ctx context.Context) error {
return s.repo.Save(context.Background())
}
Isto quebra a propagação de cancelamento.
Anti-padrão 3: Esquecendo cancel
Ruim:
ctx, _ := context.WithTimeout(parent, time.Second)
Bom:
ctx, cancel := context.WithTimeout(parent, time.Second)
defer cancel()
Anti-padrão 4: Colocando parâmetros opcionais em contexto
Ruim:
ctx = context.WithValue(ctx, "includeDeleted", true)
Use structs de opções explícitos.
Anti-padrão 5: Passando contexto muito profundamente em código puro
Ruim:
func Add(ctx context.Context, a, b int) int {
return a + b
}
Cálculo puro não precisa de contexto a menos que seja de longa duração ou cancelável.
Anti-padrão 6: Ignorando cancelamento em loops
Ruim:
for item := range items {
process(item)
}
Melhor:
for item := range items {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if err := process(ctx, item); err != nil {
return err
}
}
Anti-padrão 7: Engolindo erros de contexto
Ruim:
if err != nil {
return errors.New("operação falhou")
}
Bom:
if err != nil {
return fmt.Errorf("operação falhou: %w", err)
}
Preserve erros de cancelamento e prazo.
Uma lista de verificação prática de contexto
Use esta lista de verificação para código backend em Go.
Assinaturas de função
- Contexto é o primeiro parâmetro.
- Contexto não é armazenado em structs de longa duração.
- Contexto não é passado para funções auxiliares puras a menos que necessário.
- Contexto nil nunca é usado.
Cancelamento
- Loops de longa duração verificam
ctx.Done(). - Goroutines têm caminho de desligamento.
- Durações de workers são atreladas a um contexto pai.
- O cancelamento de contexto é propagado para chamadas downstream.
Timeouts
- Timeouts de requisição externa são definidos na fronteira.
- Timeouts de sub-operações são menores que o orçamento externo.
- Funções cancel são sempre chamadas.
- Timeouts não são empilhados cegamente em cada camada.
Valores
- Valores de contexto são com escopo de requisição.
- Chaves usam tipos customizados, não strings simples.
- Dependências não são armazenadas em contexto.
- Parâmetros opcionais não são armazenados em contexto.
Erros
context.Canceledecontext.DeadlineExceededsão preservados.- Erros de contexto são mapeados corretamente nas fronteiras da API.
- Cancelamento com conhecimento da causa é usado apenas quando o motivo importa.
Testes
- Testes usam
t.Context()onde apropriado. - Testes de timeout evitam sleeps reais lentos.
- Comportamento de timeout concorrente é testado com
testing/synctestquando útil. - Vazamentos de goroutines são verificados garantindo que caminhos de desligamento existam.
Como auditar o uso de contexto em uma base de código Go
Procure por estes padrões:
grep -R "context.Background()" .
grep -R "context.TODO()" .
grep -R "WithTimeout" .
grep -R "WithCancel" .
grep -R "WithValue" .
grep -R "type .* struct" .
E então pergunte:
context.Background()é usado apenas em fronteiras de nível superior?- Funções cancel são sempre chamadas?
- Timeouts são colocados em fronteiras sensatas?
- Valores de contexto são genuinamente com escopo de requisição?
- Dependências estão ocultas em valores de contexto?
- Goroutines podem ser paradas?
- Erros de contexto são preservados?
Isto é um bom hábito de revisão de código, porque muitos bugs de contexto não são bugs de sintaxe — são bugs de duração que só aparecem sob cancelamento, carga ou condições de desligamento.
Minhas regras opinativas
Estas regras são chatas, mas funcionam.
Regra 1: Contexto é fluxo de controle
Use contexto para controlar cancelamento, prazos e metadados de requisição.
Não o use para contrabandear dependências.
Regra 2: O chamador possui o orçamento
Uma função geralmente deve respeitar o contexto que recebe.
Crie apenas um timeout filho mais curto quando a sub-operação necessitar de um orçamento específico mais pequeno.
Regra 3: Background pertence na borda
Use context.Background() em main, testes e configuração de nível superior.
Não o use dentro de métodos de serviço e repositório para escapar do cancelamento.
Regra 4: Valores devem ser chatos
ID de requisição, ID de rastro, ID de usuário e ID de inquilino pertencem ao contexto. Conexões de banco de dados, loggers, structs de configuração e clientes de serviço não — eles são dependências e devem ser passados explicitamente.
Regra 5: Toda goroutine precisa de uma duração
Se uma goroutine inicia, você deve saber exatamente como ela para. Contexto é muitas vezes a resposta certa, e se não for contexto, deve haver algum outro mecanismo claro — um canal, um primitivo de sincronização ou um sinal explícito.
Reflexões finais
context.Context não é complicado porque a API é grande — a API é pequena. É complicado porque representa duração, e duração é arquitetura. Cada decisão sobre onde o contexto flui, onde é derivado e onde para é uma decisão sobre como seu serviço lida com falha, carga e desligamento.
Um contexto bem usado torna serviços Go mais fáceis de cancelar, mais fáceis de desligar, mais fáceis de observar e menos propensos a vazar goroutines. Um contexto mal usado oculta dependências, descarta prazos e torna o código mais difícil de raciocinar sob pressão.
O resumo prático é simples:
Passe contexto para baixo.
Não o armazene.
Não substitua parâmetros explícitos por valores.
Respeite o cancelamento.
Use timeouts nas fronteiras.
Sempre chame cancel.
Isto é contexto Go feito certo.
Este artigo faz parte do cluster Arquitetura de Aplicação em Produção, que cobre estrutura de código, acesso a dados, padrões de integração e arquitetura de testes para sistemas de produção Go e Python.