Skip to main content


Select básico​


O select é uma estrutura do Go que permite aguardar operações de envio ou recebimento em vários channels ao mesmo tempo, executando aquela que estiver disponível.


Diferentemente do switch, que verifica condições ou valores, o select trabalha com operações de channels. Isso é útil quando temos várias goroutines enviando informações e precisamos receber os dados conforme ficam disponíveis.


Imagine que temos dois channels, um responsável por receber informações de usuários e outro por receber informações de produtos.

package main

import "fmt"

func main() {
// Cria dois channels com buffer.
usuarios := make(chan string, 1)
produtos := make(chan string, 1)

// Envia informações para os channels.
usuarios <- "Usuários encontrados"
produtos <- "Produtos encontrados"

// Aguarda uma operação disponível.
select {
case mensagem := <-usuarios:
fmt.Println(mensagem)

case mensagem := <-produtos:
fmt.Println(mensagem)
}
}

Resultado possível:

Usuários encontrados

Ou:

Produtos encontrados

Nesse exemplo, usamos select para verificar qual dos channels possui uma operação de recebimento disponível. Como os dois channels já possuem valores, o Go escolhe um dos casos de forma pseudoaleatória, sem garantir qual será executado primeiro.


O select executa apenas um dos casos disponíveis por vez, então o valor do outro channel continuará armazenado no buffer. Se nenhum channel estiver pronto para a operação, o select ficará bloqueado até que algum deles esteja disponível.



Usando default no select​


Também podemos adicionar um default, que será executado quando nenhuma das operações dos channels estiver disponível.

package main

import "fmt"

func main() {
ch := make(chan int, 1)

select {
case numero := <-ch:
fmt.Println("Número recebido:", numero)

default:
fmt.Println("Nenhum valor disponível")
}
}

Resultado:

Nenhum valor disponível

Nesse exemplo, como não enviamos nenhum valor para o channel, o default será executado imediatamente, sem precisar aguardar um recebimento.


Isso significa que um select com default não bloqueia quando nenhuma operação está disponível, diferentemente de um select sem default.


Esse comportamento é útil quando queremos verificar se existem dados disponíveis, mas precisamos tomar cuidado ao colocar um select com default dentro de um laço, porque ele pode executar repetidamente sem esperar, consumindo processamento desnecessário.


O select é muito comum em programas concorrentes que trabalham com vários channels, principalmente quando precisamos aguardar respostas de diferentes goroutines, controlar o recebimento de dados ou implementar operações com timeout e cancelamento.



Context​


O context é um package do Go que permite controlar a execução de operações, principalmente quando trabalhamos com goroutines, requisições HTTP e outras tarefas concorrentes.


Ele serve para transmitir sinais de cancelamento, definir limites de tempo e compartilhar informações relacionadas a uma operação entre diferentes funções.


Por exemplo, imagine que uma goroutine esteja consultando informações de uma API, mas o usuário cancele a operação antes que ela termine. Com context, podemos avisar essa goroutine que o trabalho não é mais necessário, permitindo que ela encerre sua execução.



Criando um context​


Para criar um contexto inicial, podemos usar context.Background(), que retorna um contexto vazio, sem cancelamento ou limite de tempo definido.

ctx := context.Background()

Esse contexto normalmente serve como ponto de partida para criar outros contextos que possuem recursos de cancelamento ou timeout. Para criar um contexto que possa ser cancelado, usamos context.WithCancel().

// Cria um contexto que pode ser cancelado.
ctx, cancelar := context.WithCancel(context.Background())

Nesse exemplo, ctx representa o contexto que podemos passar para outras funções, enquanto cancelar é uma função que permite enviar um sinal de cancelamento.



Cancelando uma operação​


Vamos criar uma goroutine que aguarda um sinal de cancelamento para encerrar sua execução.

package main

import (
"context"
"fmt"
"sync"
)

func executarTarefa(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()

fmt.Println("Tarefa iniciada")

// Aguarda o sinal de cancelamento.
<-ctx.Done()

fmt.Println("Tarefa encerrada:", ctx.Err())
}

func main() {
// Cria um contexto que pode ser cancelado.
ctx, cancelar := context.WithCancel(context.Background())

var wg sync.WaitGroup
wg.Add(1)

// Inicia a tarefa passando o contexto.
go executarTarefa(ctx, &wg)

// Envia o sinal de cancelamento.
cancelar()

// Aguarda a goroutine terminar.
wg.Wait()
}

Resultado:

Tarefa iniciada
Tarefa encerrada: context canceled

Nesse exemplo, usamos context.WithCancel() para criar um contexto que pode ser cancelado. Depois, passamos esse contexto para a função executarTarefa(), que será executada em uma goroutine.


Dentro da função, temos:

<-ctx.Done()

O método Done() retorna um channel que será fechado quando o contexto for cancelado. Como estamos aguardando esse channel, a goroutine ficará bloqueada até receber o sinal de cancelamento. Quando chamamos:

cancelar()

O contexto é cancelado e o channel retornado por Done() é fechado, permitindo que a goroutine continue sua execução. Também podemos consultar o motivo do encerramento usando ctx.Err(), que retorna context.Canceled quando o contexto foi cancelado dessa forma.



O context encerra uma goroutine automaticamente?​


Um context não encerra uma goroutine diretamente, ele apenas informa que a operação deve ser cancelada. A função que recebe o contexto precisa verificar esse sinal e decidir como encerrar seu trabalho. Isso pode ser feito aguardando ctx.Done() ou verificando o cancelamento dentro de um select.


Por isso, em operações mais longas, é comum passar o context.Context como primeiro parâmetro das funções que precisam responder a cancelamentos.

func buscarUsuarios(ctx context.Context) {
// Executa uma operação que pode ser cancelada.
}

Assim, diferentes funções e goroutines podem receber o mesmo contexto e responder ao sinal de cancelamento.


Além do cancelamento manual, o package context também permite definir um tempo máximo para uma operação usando context.WithTimeout(). Quando esse tempo termina, o contexto é cancelado automaticamente, permitindo que as funções interrompam trabalhos que não precisam mais continuar. Esse comportamento será estudado no próximo tópico, Timeout e cancelamento.



Timeout com context​


O timeout permite definir um tempo máximo para uma operação. Em Go, podemos fazer isso usando context.WithTimeout(), que cria um contexto com um limite de tempo e envia automaticamente um sinal de cancelamento quando esse tempo é ultrapassado.


Isso é útil quando fazemos requisições HTTP, consultas a bancos de dados ou outras operações que podem demorar para responder, evitando que o programa fique aguardando indefinidamente.



Criando um context com timeout​


Para criar um contexto com timeout, usamos context.WithTimeout(), informando o contexto de origem e o tempo máximo permitido para a operação.

// Cria um contexto com timeout de 2 segundos.
ctx, cancelar := context.WithTimeout(
context.Background(),
2*time.Second,
)

// Libera os recursos do contexto ao terminar a função.
defer cancelar()

Nesse exemplo, o contexto será cancelado automaticamente após dois segundos, caso não tenha sido cancelado antes.


Mesmo com o cancelamento automático, usamos defer cancelar() para liberar os recursos associados ao contexto quando a função terminar, principalmente se a operação for concluída antes do timeout.



Exemplo de timeout​


Vamos simular uma operação que demora cinco segundos, mas definir um tempo máximo de dois segundos para aguardar sua conclusão.

package main

import (
"context"
"fmt"
"time"
)

func main() {
// Cria um contexto com timeout de 2 segundos.
ctx, cancelar := context.WithTimeout(
context.Background(),
2*time.Second,
)

defer cancelar()

fmt.Println("Iniciando operação...")

select {
// Simula uma operação que demora 5 segundos.
case <-time.After(5 * time.Second):
fmt.Println("Operação concluída!")

// Aguarda o cancelamento ou timeout do contexto.
case <-ctx.Done():
fmt.Println("Operação interrompida:", ctx.Err())
}
}

Resultado:

Iniciando operação...
Operação interrompida: context deadline exceeded

Nesse exemplo, time.After() retorna um channel que ficará disponível após cinco segundos, simulando o tempo necessário para concluir uma operação. Porém, como definimos um timeout de dois segundos, o contexto será cancelado antes que esse tempo termine.


O select identifica que ctx.Done() ficou disponível e executa o código responsável por tratar o encerramento. Isso faz com que o programa deixe de aguardar a operação após aproximadamente dois segundos, sem precisar esperar os cinco segundos definidos em time.After().



Identificando o motivo do cancelamento​


Quando trabalhamos com contextos, podemos verificar o motivo do encerramento por meio de ctx.Err(). Em Go, os dois resultados mais comuns são context.Canceled, que indica um cancelamento solicitado, e context.DeadlineExceeded, que indica que o tempo limite foi ultrapassado.


Podemos verificar especificamente o timeout usando:

if ctx.Err() == context.DeadlineExceeded {
fmt.Println("A operação ultrapassou o tempo limite.")
}

O context não interrompe automaticamente o código que está sendo executado, ele apenas envia um sinal de cancelamento. Por isso, a operação precisa acompanhar esse sinal para conseguir encerrar seu trabalho.


No exemplo anterior, o select interrompe a espera quando o timeout acontece. Em operações reais, como requisições HTTP, precisamos passar o contexto para as funções responsáveis pela operação, permitindo que elas também respondam ao cancelamento.