O artigo anterior introduziu o select como mecanismo para aguardar múltiplos channels. Este artigo aprofunda esse recurso e explora os padrões que emergem dele: timeouts precisos, cancelamento gracioso de operações e controle de fluxo em sistemas concorrentes.
Esses padrões são o núcleo de como serviços Go reais lidam com latência, falhas e encerramento controlado. Dominá-los é o que separa código Go funcional de código Go robusto.
O select em profundidade
O select bloqueia até que um de seus cases esteja pronto. Se múltiplos estiverem prontos simultaneamente, Go escolhe um aleatoriamente — uma garantia de imparcialidade que evita starvation:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string, 1)
ch2 := make(chan string, 1)
ch1 <- "um"
ch2 <- "dois"
// Ambos prontos — Go escolhe aleatoriamente
for i := 0; i < 4; i++ {
select {
case msg := <-ch1:
fmt.Println("ch1:", msg)
ch1 <- "um"
case msg := <-ch2:
fmt.Println("ch2:", msg)
ch2 <- "dois"
}
}
}
Executar esse programa múltiplas vezes produzirá ordens diferentes — a aleatoriedade é intencional e documentada na especificação da linguagem.
Select em loop: polling e event loops
O select dentro de um for cria um event loop — a estrutura central de muitos servidores, workers e agentes:
package main
import (
"fmt"
"time"
)
type Evento struct {
Tipo string
Payload string
}
func processarEventos(
eventos <-chan Evento,
erros <-chan error,
done <-chan struct{},
) {
for {
select {
case evento, ok := <-eventos:
if !ok {
fmt.Println("channel de eventos fechado")
return
}
fmt.Printf("evento [%s]: %s\n", evento.Tipo, evento.Payload)
case err, ok := <-erros:
if !ok {
fmt.Println("channel de erros fechado")
return
}
fmt.Printf("erro recebido: %v\n", err)
case <-done:
fmt.Println("encerramento solicitado")
return
}
}
}
func main() {
eventos := make(chan Evento, 5)
erros := make(chan error, 5)
done := make(chan struct{})
go processarEventos(eventos, erros, done)
eventos <- Evento{"compra", "pedido #1001"}
eventos <- Evento{"login", "usuário ana@exemplo.com"}
time.Sleep(50 * time.Millisecond)
close(done)
time.Sleep(50 * time.Millisecond)
}
Timeouts com time.After
time.After retorna um <-chan Time que recebe um valor após a duração especificada. Combinado com select, implementa timeouts de forma expressiva:
package main
import (
"fmt"
"time"
)
func consultarBancoDados(query string) <-chan string {
resultado := make(chan string, 1)
go func() {
// Simula latência variável
time.Sleep(300 * time.Millisecond)
resultado <- fmt.Sprintf("dados para: %s", query)
}()
return resultado
}
func executarComTimeout(query string, limite time.Duration) (string, error) {
resultado := consultarBancoDados(query)
select {
case dados := <-resultado:
return dados, nil
case <-time.After(limite):
return "", fmt.Errorf("timeout após %v: query=%s", limite, query)
}
}
func main() {
// Timeout generoso — deve ter sucesso
dados, err := executarComTimeout("SELECT * FROM usuarios", 500*time.Millisecond)
if err != nil {
fmt.Println("Erro:", err)
} else {
fmt.Println("Sucesso:", dados)
}
// Timeout apertado — deve falhar
dados, err = executarComTimeout("SELECT * FROM pedidos", 100*time.Millisecond)
if err != nil {
fmt.Println("Erro:", err)
} else {
fmt.Println("Sucesso:", dados)
}
}
O problema com time.After em loops
time.After cria um novo timer a cada chamada. Dentro de loops de alta frequência, timers antigos não são coletados pelo GC até dispararem, causando acúmulo de memória. Para loops, use time.NewTimer com reset explícito:
package main
import (
"fmt"
"time"
)
func processarComTimeout(ch <-chan int, timeout time.Duration) {
timer := time.NewTimer(timeout)
defer timer.Stop() // libera o timer se a função retornar antes
for {
// Resetar o timer a cada iteração
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
timer.Reset(timeout)
select {
case v, ok := <-ch:
if !ok {
fmt.Println("channel fechado")
return
}
fmt.Println("processado:", v)
case <-timer.C:
fmt.Println("timeout — nenhum dado recebido")
return
}
}
}
func main() {
ch := make(chan int)
go func() {
for i := 1; i <= 3; i++ {
time.Sleep(100 * time.Millisecond)
ch <- i
}
close(ch)
}()
processarComTimeout(ch, 200*time.Millisecond)
}
Ticker: ações periódicas
time.Ticker dispara em intervalos regulares — útil para polling, heartbeats e tarefas agendadas:
package main
import (
"fmt"
"time"
)
func monitorar(done <-chan struct{}) {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case t := <-ticker.C:
fmt.Printf("verificação em %s\n", t.Format("15:04:05.000"))
case <-done:
fmt.Println("monitoramento encerrado")
return
}
}
}
func main() {
done := make(chan struct{})
go monitorar(done)
time.Sleep(2 * time.Second)
close(done)
time.Sleep(100 * time.Millisecond)
}
Cancelamento com context
O pacote context é a solução idiomática e recomendada para cancelamento em Go. Ele propaga cancelamento, deadlines e valores pela árvore de chamadas de função — incluindo através de limites de goroutines, chamadas HTTP e consultas a banco de dados.
package main
import (
"context"
"fmt"
"time"
)
func operacaoLonga(ctx context.Context, id int) error {
select {
case <-time.After(500 * time.Millisecond):
fmt.Printf("operação %d concluída\n", id)
return nil
case <-ctx.Done():
fmt.Printf("operação %d cancelada: %v\n", id, ctx.Err())
return ctx.Err()
}
}
func main() {
// Context com cancelamento manual
ctx, cancelar := context.WithCancel(context.Background())
go func() {
time.Sleep(200 * time.Millisecond)
cancelar() // cancela após 200ms
}()
if err := operacaoLonga(ctx, 1); err != nil {
fmt.Println("Erro:", err) // context canceled
}
// Context com deadline absoluto
prazo := time.Now().Add(300 * time.Millisecond)
ctxDeadline, cancelDeadline := context.WithDeadline(context.Background(), prazo)
defer cancelDeadline()
if err := operacaoLonga(ctxDeadline, 2); err != nil {
fmt.Println("Erro:", err) // context deadline exceeded
}
// Context com timeout relativo — mais comum que WithDeadline
ctxTimeout, cancelTimeout := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancelTimeout()
if err := operacaoLonga(ctxTimeout, 3); err != nil {
fmt.Println("Erro:", err) // context deadline exceeded
}
}
Sempre chame a função de cancelamento retornada por WithCancel, WithDeadline e WithTimeout — mesmo que o contexto já tenha sido cancelado. Isso libera recursos internos. O defer cancelar() logo após a criação do contexto é o padrão correto.
Propagando context pela árvore de chamadas
O context forma uma árvore: cancelar um contexto pai cancela todos os filhos automaticamente. Isso permite cancelamento em cascata sem comunicação explícita entre goroutines:
package main
import (
"context"
"fmt"
"time"
)
func buscarUsuario(ctx context.Context, id int) (string, error) {
select {
case <-time.After(100 * time.Millisecond):
return fmt.Sprintf("usuario_%d", id), nil
case <-ctx.Done():
return "", fmt.Errorf("buscarUsuario: %w", ctx.Err())
}
}
func buscarPedidos(ctx context.Context, usuarioID int) ([]string, error) {
select {
case <-time.After(200 * time.Millisecond):
return []string{"pedido_1", "pedido_2"}, nil
case <-ctx.Done():
return nil, fmt.Errorf("buscarPedidos: %w", ctx.Err())
}
}
func carregarPerfil(ctx context.Context, userID int) error {
// Contexto filho com timeout mais restrito para cada operação
ctxDB, cancelar := context.WithTimeout(ctx, 150*time.Millisecond)
defer cancelar()
usuario, err := buscarUsuario(ctxDB, userID)
if err != nil {
return err
}
pedidos, err := buscarPedidos(ctxDB, userID)
if err != nil {
return err
}
fmt.Printf("Perfil carregado: %s com %d pedidos\n", usuario, len(pedidos))
return nil
}
func main() {
// Contexto raiz com timeout global de 500ms
ctx, cancelar := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancelar()
if err := carregarPerfil(ctx, 42); err != nil {
fmt.Println("Erro:", err)
}
}
Verificando cancelamento sem bloquear
Em loops de processamento, verificar o context sem bloquear usa select com default:
func processarItens(ctx context.Context, itens []string) error {
for _, item := range itens {
// Verifica cancelamento antes de cada item
select {
case <-ctx.Done():
return fmt.Errorf("processamento cancelado após %d itens: %w",
len(itens), ctx.Err())
default:
// continua
}
// Processamento do item
fmt.Println("processando:", item)
time.Sleep(50 * time.Millisecond)
}
return nil
}
Encerramento gracioso de servidores HTTP
Um dos casos de uso mais importantes de cancelamento em produção é o graceful shutdown — encerrar um servidor HTTP sem interromper requisições em andamento:
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(100 * time.Millisecond) // simula processamento
fmt.Fprintln(w, "OK")
})
servidor := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Canal para capturar sinais do sistema operacional
sinal := make(chan os.Signal, 1)
signal.Notify(sinal, syscall.SIGINT, syscall.SIGTERM)
// Inicia servidor em goroutine separada
go func() {
fmt.Println("Servidor iniciado em :8080")
if err := servidor.ListenAndServe(); err != http.ErrServerClosed {
fmt.Println("Erro no servidor:", err)
}
}()
// Aguarda sinal de encerramento
<-sinal
fmt.Println("\nSinal recebido — encerrando...")
// Contexto com 30 segundos para finalizar requisições em andamento
ctx, cancelar := context.WithTimeout(context.Background(), 30*time.Second)
defer cancelar()
if err := servidor.Shutdown(ctx); err != nil {
fmt.Println("Erro no shutdown:", err)
} else {
fmt.Println("Servidor encerrado com sucesso")
}
}
Padrão: retry com context e backoff
Combinando context e select, é possível implementar retry com backoff exponencial e cancelamento:
package main
import (
"context"
"fmt"
"time"
)
func operacaoComRetry(ctx context.Context, maxTentativas int) error {
espera := 100 * time.Millisecond
for tentativa := 1; tentativa <= maxTentativas; tentativa++ {
// Verifica cancelamento antes de tentar
select {
case <-ctx.Done():
return fmt.Errorf("cancelado antes da tentativa %d: %w",
tentativa, ctx.Err())
default:
}
err := tentarOperacao()
if err == nil {
fmt.Printf("sucesso na tentativa %d\n", tentativa)
return nil
}
fmt.Printf("tentativa %d falhou: %v\n", tentativa, err)
if tentativa == maxTentativas {
return fmt.Errorf("todas as %d tentativas falharam", maxTentativas)
}
// Aguarda com cancelamento
select {
case <-ctx.Done():
return fmt.Errorf("cancelado durante espera: %w", ctx.Err())
case <-time.After(espera):
espera *= 2 // backoff exponencial
}
}
return nil
}
var contador int
func tentarOperacao() error {
contador++
if contador < 3 {
return fmt.Errorf("serviço indisponível")
}
return nil
}
func main() {
ctx, cancelar := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelar()
if err := operacaoComRetry(ctx, 5); err != nil {
fmt.Println("Erro final:", err)
}
}
Resumo do que foi coberto
Este artigo aprofundou o select e os padrões de cancelamento em Go: o select em loops como event loop, timeouts com time.After e time.NewTimer, o time.Ticker para ações periódicas, cancelamento com context.WithCancel, context.WithDeadline e context.WithTimeout, propagação de context pela árvore de chamadas, verificação não bloqueante de cancelamento, encerramento gracioso de servidores HTTP e retry com backoff exponencial. O próximo artigo aborda sync.WaitGroup e sync.Mutex para sincronização de goroutines com memória compartilhada.
Referências e leituras complementares
-
Documentação do pacote context — Referência completa de WithCancel, WithTimeout e WithDeadline. https://pkg.go.dev/context
-
Go Blog: Go Concurrency Patterns: Context — Artigo oficial sobre o pacote context. https://go.dev/blog/context
-
Go by Example: Timeouts — Exemplos práticos de timeout com select. https://gobyexample.com/timeouts
-
Go by Example: Tickers — Exemplos de time.Ticker para ações periódicas. https://gobyexample.com/tickers
-
Go by Example: Context — Exemplos de cancelamento com context. https://gobyexample.com/context
-
Documentação net/http — Server.Shutdown — Referência do encerramento gracioso de servidores. https://pkg.go.dev/net/http#Server.Shutdown