Pular para o conteúdo principal

victorstein.dev

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 no select.
  • Err(): depois que o Done() fecha, diz o motivo: context.Canceled ou context.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.Sleep no final do main é só para dar tempo dos workers imprimirem a mensagem. Num código real usaríamos um sync.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:

  1. 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".
  2. 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 Value retorna any, 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 nil como context. Se não souber qual usar, use context.TODO().
  • Sempre chame o cancel, de preferência com defer logo 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.

# Referências

  1. Documentação Context
  2. Go Concurrency Patterns: Context