Esperando por vários channels
select parece um switch, mas cada case é um envio ou um recebimento de channel. Ele bloqueia até que um case possa prosseguir e então executa esse case.
Com esses atrasos, o primeiro select pega o resultado rápido e o segundo pega o lento. Nenhum dos recebimentos precisa esperar pelo outro channel. Um <-slow simples seguido de <-fast os trataria em uma ordem fixa, independentemente de qual chegasse primeiro.
Como o select avalia:
- Todas as expressões de channel e os valores a enviar são avaliados uma vez, na ordem do código, quando o
selectcomeça. - Se um ou mais cases estiverem prontos, um deles é escolhido ao acaso.
- Se nenhum estiver pronto e houver um
default, odefaultexecuta. - Caso contrário, a goroutine bloqueia até algum case ficar pronto.
Um select {} vazio bloqueia para sempre. Às vezes você o vê no fim do main em programas cujo trabalho real acontece em outras goroutines.
Escolha aleatória entre cases prontos
Quando vários cases estão prontos ao mesmo tempo, o select não prefere o primeiro da lista. Este programa enche dois channels com buffer e depois faz select 1000 vezes:
A divisão entre countA e countB muda a cada execução e fica perto de 500 para cada um. A escolha aleatória é proposital: ela impede que um channel movimentado deixe os outros famintos. Se você precisa de prioridade, veja o padrão mais abaixo.
Operações sem bloqueio com default
Com um case default, o select nunca bloqueia. Isso transforma um envio ou um recebimento em uma operação de "tentativa":
Descartar trabalho quando um buffer está cheio é como você alivia a carga ou emite métricas sem nunca travar quem chama.
Não coloque um default em um select dentro de um laço for só para "consultar" os channels repetidamente. Com nada pronto, o laço gira a 100% de CPU. Bloqueie, e acrescente um case de timeout se precisar acordar de tempos em tempos.
Timeouts
time.After(d) devolve um channel que recebe uma vez depois de d. Coloque-o para disputar com o trabalho real:
A primeira chamada devolve "data". A segunda devolve o erro de timeout depois de 50 ms, bem antes de o worker terminar. O channel de resultado tem buffer de 1 de propósito. Quando o timeout vence, ninguém nunca recebe de result; com um channel sem buffer, a goroutine do worker ficaria bloqueada no envio para sempre e vazaria.
Em um laço, time.After cria um timer novo a cada iteração, o que é exatamente o certo para um timeout de inatividade por mensagem ("nenhuma mensagem por 1 segundo"). Para um prazo geral de muitas operações, crie um timer ou um context uma vez, antes do laço. Desde o Go 1.23, timers que não são mais referenciados são recolhidos pelo coletor de lixo mesmo que não tenham disparado, então time.After em um laço não segura mais memória até cada timer disparar, como acontecia em versões antigas (isso exige go 1.23 ou mais no go.mod).
Laços for-select e channels de parada
Uma goroutine que executa até mandarem parar é um laço for em volta de um select com um case para o trabalho e um para a parada:
Fechar quit em vez de enviar nele é o idioma: um close é visto por todos os receptores, agora e depois, então um único close para qualquer número de workers. O channel done permite que o main espere até o worker realmente ter retornado.
Em código real, o channel de parada normalmente é um context.Context: case <-ctx.Done():. Funciona do mesmo jeito (Done() devolve um channel que é fechado no cancelamento) e ainda carrega prazos e o motivo da parada. A página de context cobre isso.
break dentro do select
break em um case de select sai do select, não do for ao redor. Essa é uma fonte comum de laços que nunca terminam. Use return, ou dê um label ao laço:
Prioridade entre channels
Como o select escolhe ao acaso, você não consegue ordenar os cases dentro de uma instrução. Para fazer um channel vencer sempre que tiver algo, verifique-o antes, sozinho:
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
select {
case <-ctx.Done():
return ctx.Err()
case job := <-jobs:
handle(job)
}
}
O primeiro select retorna na hora se o cancelamento já aconteceu. Sem ele, um fluxo constante de jobs poderia continuar vencendo a escolha aleatória por um tempo depois de ctx ter sido cancelado.
Channels nil desativam um case
Um envio ou recebimento em um channel nil nunca fica pronto, então um case sobre um channel nil fica, na prática, desligado. Definir uma entrada fechada como nil é como você para de fazer select nela e continua com as outras; a página de channels mostra um laço de mesclagem montado assim. O mesmo truque liga e desliga um timeout: mantenha var timeout <-chan time.Time como nil até precisar dele, e depois atribua time.After(d).
Erros comuns
- Esperar a ordem do código. O primeiro case da lista não tem preferência.
- Um laço ocupado com
default. Umfor { select { ... default: } }sem nada para fazer queima um núcleo de CPU. - Vazar o perdedor. Quando um timeout vence, a goroutine que enviaria o resultado ainda precisa conseguir terminar. Dê ao channel dela um buffer de 1.
- Um
breakque só sai doselect. Use um label oureturn. - Um
time.Afterpor iteração usado como prazo geral. Ele recomeça a cada iteração; crie o prazo uma vez, fora do laço.
Perguntas frequentes
O que o select faz em Go?
select espera até que uma das suas operações de channel (um envio ou um recebimento) possa prosseguir, e então executa esse case. Se vários estiverem prontos ao mesmo tempo, ele escolhe um ao acaso. Sem um case default ele bloqueia até algum case ficar pronto; com default ele nunca bloqueia.
Como colocar um timeout no recebimento de um channel em Go?
Coloque o recebimento e um timer no mesmo select: select { case v := <-ch: use(v); case <-time.After(2 * time.Second): return errTimeout }. O que acontecer primeiro vence. Dentro de um laço, ou quando quem chama já tem um prazo, use um context.Context com context.WithTimeout e faça select em ctx.Done().
O select do Go escolhe os cases em ordem?
Não. Quando mais de um case está pronto, o Go escolhe de forma uniforme e aleatória, para que nenhum case deixe os outros famintos. Se você precisa de prioridade, verifique antes o channel de alta prioridade em um select próprio com default, e depois caia em um select sobre todos os channels.
Por que o break não sai do meu laço for-select?
Dentro de um select, o break só sai da instrução select, não do for ao redor. Use return, ou coloque um label no laço (loop: for { select { case <-done: break loop } }).