Golang

Select, timeouts e cancelamento de operações Já leu

9 min de leitura

Select, timeouts e cancelamento de operações
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 con

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

Comentários

Mais em Golang

Goroutines: concorrência leve e o modelo de execução do Go
Goroutines: concorrência leve e o modelo de execução do Go

Se existe um recurso que mais distingue Go de outras linguagens modernas, é s...

Artigo 42 — Documentação com godoc e boas práticas de comentário
Artigo 42 — Documentação com godoc e boas práticas de comentário

Artigo 42 — Documentação com godoc e boas práticas de comentário Documentação...

Artigo 51 — gRPC e Protocol Buffers: comunicação eficiente entre serviços
Artigo 51 — gRPC e Protocol Buffers: comunicação eficiente entre serviços

Artigo 51 — gRPC e Protocol Buffers: comunicação eficiente entre serviços Alé...