Done channel e cancelamento
1. Fundamentos do Done Channel
O padrão done channel é uma das técnicas mais elegantes em Go para sinalizar cancelamento entre goroutines. A ideia central é utilizar um channel do tipo struct{} (vazio) que, quando fechado, avisa a todos os ouvintes que devem encerrar suas operações.
package main
import (
"fmt"
"time"
)
func worker(done <-chan struct{}) {
for {
select {
case <-done:
fmt.Println("Worker finalizado")
return
default:
fmt.Println("Trabalhando...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
done := make(chan struct{})
go worker(done)
time.Sleep(2 * time.Second)
close(done)
time.Sleep(200 * time.Millisecond) // aguarda finalização
}
2. Criação e Propagação do Sinal de Cancelamento
Fechar um channel (close(done)) é a operação chave: ela envia um sinal de broadcast para todas as goroutines que estão escutando aquele channel. Diferente de enviar um valor, que só é consumido por um receptor, close libera todos os seletores simultaneamente.
func propagateCancel(parentDone <-chan struct{}) <-chan struct{} {
childDone := make(chan struct{})
go func() {
<-parentDone
close(childDone)
}()
return childDone
}
A propagação por parâmetro cria um contexto implícito de cancelamento, onde cada goroutine recebe o done channel e decide como reagir.
3. O Pacote context como Evolução do Done Channel
O pacote context padronizou e estendeu o padrão done channel. O método Done() retorna <-chan struct{}, exatamente como nosso done channel manual.
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Worker cancelado:", ctx.Err())
return
default:
fmt.Println("Processando...")
time.Sleep(300 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
time.Sleep(1 * time.Second)
cancel()
time.Sleep(100 * time.Millisecond)
}
A hierarquia de contextos é poderosa: quando um contexto pai é cancelado, todos os contextos filhos também são cancelados automaticamente.
4. Padrões de Cancelamento em Cascata
Com context.WithCancel, um único cancel() pode finalizar dezenas de goroutines simultaneamente.
func startWorkers(ctx context.Context, n int) {
for i := 0; i < n; i++ {
go func(id int) {
select {
case <-ctx.Done():
fmt.Printf("Worker %d encerrado\n", id)
}
}(i)
}
}
Um exemplo prático é o shutdown graceful de um servidor HTTP:
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
fmt.Println("Sinal recebido, cancelando...")
cancel()
}()
srv := &http.Server{Addr: ":8080"}
go func() {
<-ctx.Done()
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
srv.Shutdown(shutdownCtx)
}()
fmt.Println("Servidor rodando...")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
panic(err)
}
}
5. Cleanup e Recursos com defer e Cancelamento
Ao receber um sinal de cancelamento, é essencial liberar recursos corretamente. A combinação de defer com verificação de ctx.Err() garante isso.
type ResourceManager struct {
conn *Connection
closed bool
}
func (rm *ResourceManager) Work(ctx context.Context) error {
rm.conn = openConnection()
defer func() {
if rm.closed {
return
}
rm.conn.Close()
}()
select {
case <-ctx.Done():
rm.closed = true
return ctx.Err()
case result := <-rm.process():
return result
}
}
func (rm *ResourceManager) process() <-chan error {
ch := make(chan error)
go func() {
// simula processamento
time.Sleep(2 * time.Second)
ch <- nil
}()
return ch
}
6. Cancelamento com Timeout e Deadline
context.WithTimeout e context.WithDeadline adicionam limites temporais ao cancelamento.
func callExternalAPI(ctx context.Context) (string, error) {
// Simula chamada externa
select {
case <-time.After(3 * time.Second):
return "resposta", nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
result, err := callExternalAPI(ctx)
if err != nil {
fmt.Printf("Erro: %v\n", err) // deadline exceeded
return
}
fmt.Println("Resultado:", result)
}
A diferença entre timeout expirado e cancelamento explícito pode ser verificada com ctx.Err():
- context.DeadlineExceeded → timeout expirou
- context.Canceled → cancel() foi chamado explicitamente
7. Erros Comuns e Boas Práticas
Nunca passe cancel() como parâmetro. A função de cancelamento deve permanecer no escopo que a criou.
// ERRADO
func process(ctx context.Context, cancel context.CancelFunc) {
// perigoso: pode cancelar o contexto de quem chamou
}
// CORRETO
func process(ctx context.Context) {
// usa apenas ctx.Done() para reagir a cancelamentos
}
Sempre chame cancel() para evitar vazamento de recursos, mesmo que o contexto já tenha expirado:
ctx, cancel := context.WithTimeout(parent, 100*time.Millisecond)
defer cancel() // essencial!
// ... uso do ctx
Verifique ctx.Err() após <-ctx.Done() para entender a causa do cancelamento:
select {
case <-ctx.Done():
switch ctx.Err() {
case context.Canceled:
fmt.Println("Cancelamento explícito")
case context.DeadlineExceeded:
fmt.Println("Timeout expirou")
}
}
O padrão done channel e sua evolução com o pacote context são fundamentais para escrever código Go concorrente robusto e controlável. Dominar esses conceitos permite criar sistemas que respondem graciosamente a cancelamentos, timeouts e sinais do sistema operacional, evitando goroutines órfãs e vazamento de recursos.
Referências
- Documentação oficial do pacote context — Referência completa da API de contextos em Go, incluindo WithCancel, WithTimeout e WithDeadline
- Go Concurrency Patterns: Context — Artigo oficial do Go Blog explicando o padrão context e cancelamento
- Cancellation in Go (The Go Programming Language) — Artigo sobre pipelines e cancelamento, com exemplos de done channels
- Contexts and structs (Dave Cheney) — Discussão aprofundada sobre boas práticas no uso de contextos
- Go by Example: Context — Exemplos práticos e didáticos de cancelamento com context
- Understanding Go's context package (Soham Kamani) — Tutorial detalhado com exemplos de cancelamento e timeout