89/100 Dias de Golang - Context
Table of Contents
#
Context
Contexts são usados para resolver problemas relacionados à propagação de cancelamento e deadlines entre goroutines. Pense que estamos executando uma tarefa e aí alguém aparece e fala que essa tarefa não é mais necessária: não temos motivo para continuar executando. Quando um context é cancelado, todas as goroutines que estão escutando ele recebem esse sinal e podem encerrar suas execuções.
Já esbarramos no context em alguns posts: nas requisições HTTP, no GORM, no Testcontainers… Quase toda biblioteca que faz I/O recebe um ctx context.Context como primeiro parâmetro. Hoje é o dia de entender o que ele faz.
Olhando a definição do context no código do Golang, temos que o context é uma interface:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
Deadline(): retorna o horário em que o context será cancelado automaticamente (se existir um).Done(): retorna um channel que é fechado quando o context é cancelado. É ele que usamos noselect.Err(): depois que oDone()fecha, diz o motivo:context.Canceledoucontext.DeadlineExceeded.Value(): busca um valor guardado no context.
#
Criando um context
Todo context começa a partir de um context “raiz”:
ctx := context.Background()
O context.Background() é vazio: nunca é cancelado, não tem deadline e nem valores. Normalmente ele é criado no main ou no início de um teste. Existe também o context.TODO(), que é igual, mas serve para indicar “ainda não sei qual context usar aqui”.
A partir dele criamos contexts “filhos” com as funções WithCancel, WithTimeout, WithDeadline e WithValue. Isso forma uma árvore: quando um pai é cancelado, todos os filhos também são, mas cancelar um filho não afeta o pai.
#
WithCancel
O WithCancel retorna um context e uma função cancel. Quando chamamos cancel(), o channel Done() é fechado.
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("worker %d: parando (%v)\n", id, ctx.Err())
return
default:
fmt.Printf("worker %d: trabalhando...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
for i := 1; i <= 3; i++ {
go worker(ctx, i)
}
time.Sleep(2 * time.Second)
fmt.Println("main: cancelando o contexto")
cancel()
time.Sleep(1 * time.Second)
fmt.Println("main: fim")
}
Saída:
worker 3: trabalhando...
worker 2: trabalhando...
worker 1: trabalhando...
...
main: cancelando o contexto
worker 1: parando (context canceled)
worker 3: parando (context canceled)
worker 2: parando (context canceled)
main: fim
Uma única chamada de cancel() parou as três goroutines. Lembra do dia 36 sobre select? É a mesma ideia de usar um channel para avisar que é hora de parar, mas agora com uma API padrão que todas as bibliotecas entendem.
O
time.Sleepno final domainé só para dar tempo dos workers imprimirem a mensagem. Num código real usaríamos umsync.WaitGroup(dia 18).
#
WithTimeout e WithDeadline
Esse é o uso mais comum no dia a dia: limitar quanto tempo uma operação pode demorar. O WithTimeout recebe uma duração e o WithDeadline recebe um horário exato; no fim das contas, WithTimeout(ctx, d) é o mesmo que WithDeadline(ctx, time.Now().Add(d)).
package main
import (
"context"
"fmt"
"time"
)
func buscarDados(ctx context.Context) (string, error) {
resultado := make(chan string, 1)
go func() {
time.Sleep(3 * time.Second) // simulando uma operação lenta
resultado <- "dados do banco"
}()
select {
case r := <-resultado:
return r, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
dados, err := buscarDados(ctx)
if err != nil {
fmt.Println("erro:", err)
return
}
fmt.Println(dados)
}
Saída:
erro: context deadline exceeded
A operação levaria 3 segundos, mas o context só deu 1. A função retornou assim que o tempo acabou, sem esperar.
Repare no defer cancel(). Mesmo que o timeout cancele o context sozinho, sempre devemos chamar o cancel: ele libera os recursos (timers) associados ao context. Se esquecer, o go vet avisa.
Outro detalhe: o channel resultado tem buffer de 1. Sem o buffer, a goroutine ficaria travada para sempre tentando enviar o resultado que ninguém mais vai ler (um goroutine leak).
#
Context em requisições HTTP
Lá nos dias 14, 15 e 16 fizemos requisições para APIs externas. Se a API demorar para responder, nosso programa fica esperando. Com context conseguimos colocar um limite:
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://httpbin.org/delay/5", nil)
if err != nil {
panic(err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println("erro na requisição:", err)
return
}
defer resp.Body.Close()
fmt.Println("status:", resp.Status)
}
O endpoint httpbin.org/delay/5 demora 5 segundos para responder. Como o context tem só 2 segundos, temos:
erro na requisição: Get "https://httpbin.org/delay/5": context deadline exceeded
A mesma ideia vale para banco de dados: o database/sql tem QueryContext e ExecContext, e no GORM usamos db.WithContext(ctx).
#
Context no servidor HTTP
Do lado do servidor, cada request já vem com um context, acessado por r.Context(). Ele é cancelado automaticamente quando o cliente fecha a conexão. Isso é ótimo para não gastar processamento com quem já foi embora:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func relatorioHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log.Println("gerando relatório...")
select {
case <-time.After(5 * time.Second): // simulando um processamento pesado
fmt.Fprintln(w, "relatório pronto!")
log.Println("relatório enviado")
case <-ctx.Done():
log.Println("cliente desistiu:", ctx.Err())
}
}
func main() {
http.HandleFunc("/relatorio", relatorioHandler)
log.Println("servidor rodando em :8089")
log.Fatal(http.ListenAndServe(":8089", nil))
}
Rodando o servidor e fazendo uma requisição que desiste depois de 1 segundo:
curl -m 1 localhost:8089/relatorio
No log do servidor:
2026/10/20 09:42:17 servidor rodando em :8089
2026/10/20 09:42:20 gerando relatório...
2026/10/20 09:42:21 cliente desistiu: context canceled
No Gin o context da request fica em c.Request.Context().
#
WithValue
O WithValue permite guardar valores dentro do context. É usado para dados que pertencem ao request, como um request ID ou o usuário autenticado:
package main
import (
"context"
"fmt"
)
type chave string
const requestIDKey chave = "requestID"
func processar(ctx context.Context) {
id, ok := ctx.Value(requestIDKey).(string)
if !ok {
id = "desconhecido"
}
fmt.Println("processando request", id)
}
func main() {
ctx := context.WithValue(context.Background(), requestIDKey, "abc-123")
processar(ctx)
processar(context.Background())
}
Saída:
processando request abc-123
processando request desconhecido
Dois cuidados aqui:
- Use um tipo próprio para a chave (o
type chave string), e não uma string pura. Assim seu valor nunca colide com o de outro pacote que também usou"requestID". - Não use o context para passar parâmetros da função. Se a função precisa de um valor para funcionar, ele deve ser um argumento. O
Valueretornaany, então perdemos a checagem de tipos do compilador.
#
Boas práticas
Resumindo as regras que aparecem na documentação:
- O context deve ser o primeiro parâmetro da função e se chamar
ctx:func Buscar(ctx context.Context, id int). - Não guarde o context dentro de uma struct, passe ele explicitamente em cada chamada.
- Nunca passe
nilcomo context. Se não souber qual usar, usecontext.TODO(). - Sempre chame o
cancel, de preferência comdeferlogo depois de criar o context. - Context é seguro para ser usado por várias goroutines ao mesmo tempo.
Nas versões mais recentes do Go surgiram algumas funções extras que valem uma olhada: context.WithCancelCause (cancela informando um erro como causa), context.AfterFunc (executa uma função quando o context é cancelado) e context.WithoutCancel (cria um filho que não é cancelado junto com o pai).
No próximo post vamos usar context e o que vimos sobre sinais no post do bot do discord para fazer um graceful shutdown: desligar um servidor sem derrubar as requisições que estão em andamento.