Menu

Channels em Golang: com buffer, sem buffer, close e range

Como os channels do Go passam valores entre goroutines: channels sem buffer e com buffer, fechar e percorrer com range, tipos com direção, o erro de deadlock e um pipeline montado com eles.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Enviando e recebendo

Um channel é um cano tipado entre goroutines. ch <- v envia, <-ch recebe. Crie um com make:

O valor zero de um tipo channel é nil, então var ch chan string sem make dá um channel que bloqueia para sempre. Sempre crie channels com make.

Channels sem buffer sincronizam

make(chan T) cria um channel sem buffer. Um envio bloqueia até um receptor pegar o valor, e um recebimento bloqueia até um remetente fornecer um. As duas goroutines se encontram nesse ponto, o que faz de um channel sem buffer uma ferramenta de sincronização tanto quanto um cano de dados. Quando <-done retorna abaixo, você sabe que o worker terminou tudo o que vinha antes do envio:

Ler result no main é seguro aqui sem mutex. O modelo de memória do Go garante que tudo o que o worker fez antes do envio fica visível para o main depois do recebimento correspondente. chan struct{} é o tipo idiomático para um sinal puro, porque struct{} não ocupa memória.

Channels com buffer

make(chan T, n) dá ao channel espaço para n valores. Os envios dão certo sem receptor até o buffer encher; os recebimentos dão certo até ele esvaziar. Os valores saem na ordem em que entraram.

Um buffer desacopla remetente e receptor para que rajadas curtas não travem o remetente. Ele não resolve um produtor que é permanentemente mais rápido que o seu consumidor; só adia o momento em que o remetente bloqueia. Escolha o tamanho do buffer por um motivo (o número de remetentes, um tamanho de lote conhecido), não para fazer um deadlock sumir.

len(ch) é um retrato do momento. Quando você age com base nele, outra goroutine pode já tê-lo mudado, então não o use para decidir se um envio vai bloquear. Use select com um case default para isso.

close e range

close(ch) avisa os receptores de que nenhum valor novo será enviado. Depois de um close:

  • os valores que já estão no buffer ainda são entregues,
  • depois disso todo recebimento devolve o valor zero na hora,
  • v, ok := <-ch informa ok == false,
  • for v := range ch termina.

O primeiro recebimento depois do close ainda pega "last" com ok == true. O segundo pega o valor zero "" e false.

Regras que causam panic:

  • enviar em um channel fechado causa panic com send on closed channel,
  • fechar um channel já fechado causa panic,
  • fechar um channel nil causa panic.

Então só o lado que envia fecha, e só uma vez. Com vários remetentes, nenhum deles sabe quando os outros terminaram; faça uma goroutine separada esperar todos os remetentes (com um sync.WaitGroup) e fechar o channel depois que o Wait retornar. Fechar só é necessário quando um receptor espera pelo fim. Um channel não fechado que ninguém referencia é recolhido pelo coletor de lixo como qualquer outro valor.

Tipos com direção

Uma função pode declarar que só envia ou só recebe em um channel. O compilador então rejeita a outra operação.

TipoSignificadoPermitido
chan Tbidirecionalenviar, receber, fechar
chan<- Tsó de envioenviar, fechar
<-chan Tsó de recebimentoreceber

Um chan T é convertido implicitamente para qualquer um dos tipos restritos quando você o passa para uma função. É por isso que produce, acima, pode devolver um <-chan int: quem chama pode percorrê-lo com range, mas não pode enviar nele nem fechá-lo. Use tipos com direção em todo parâmetro de função em que se aplicam. Eles documentam quem é dono do channel e transformam o uso errado em erro de compilação.

Deadlock

Se todas as goroutines estão bloqueadas e nada pode acordar nenhuma delas, o runtime aborta o programa:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	/tmp/main.go:7 +0x38
exit status 2

O dump de goroutines diz qual operação está travada (chan send aqui, ou chan receive, sync.WaitGroup.Wait, select). As causas de sempre:

  • um envio em um channel sem buffer sem nenhum receptor rodando,
  • range sobre um channel que nunca é fechado,
  • um contador de WaitGroup que nunca chega a zero,
  • duas goroutines esperando uma pela outra.

O runtime só detecta o caso em que todas as goroutines estão travadas. Em um servidor com outras goroutines vivas (um listener HTTP, um ticker), o mesmo bug não derruba nada; a goroutine bloqueada apenas vaza.

Channels nil

Envios e recebimentos em um channel nil bloqueiam para sempre. Parece inútil, mas é um truque padrão dentro de um select: definir uma variável de channel como nil desativa o case dela. Isto mescla dois channels e para de escutar cada um assim que ele é fechado:

Sem as atribuições de nil, um channel fechado está sempre pronto, e o laço ficaria girando sobre valores zero.

Um pipeline

Channels se combinam em pipelines: cada estágio é uma goroutine que recebe de um channel e envia para o próximo, e fecha a sua saída quando a entrada acaba.

Cada estágio executa de forma concorrente, e a ordem da saída é determinística (1, 16, 81) porque cada estágio é uma única goroutine que preserva a ordem. Os fechamentos se propagam em cascata: generate fecha, o que encerra o range de square, que fecha a sua saída, e assim por diante até o main.

O ponto fraco deste pipeline: se o main parasse de ler antes do fim, os estágios ficariam bloqueados nos seus envios para sempre. Pipelines reais recebem um context.Context ou um channel done e fazem select nele ao lado de cada envio.

Channel ou mutex

Channels servem para passar a posse de dados e para sinalizar eventos. Um sync.Mutex é mais simples para proteger estado compartilhado que muitas goroutines leem e atualizam no lugar, como um cache ou um contador. Uma struct com um mutex dentro muitas vezes é mais clara que uma goroutine dona do estado atendendo requisições por channels. Use o que deixar o código mais curto e a posse dos dados óbvia.

Referência rápida

Operaçãochannel nilchannel abertochannel fechado
ch <- vbloqueia para semprebloqueia até ser recebido ou haver espaço no bufferpanic
<-chbloqueia para semprebloqueia até haver um valor disponívelvalores do buffer, depois o valor zero
v, ok := <-chbloqueia para sempreok é trueok é false depois de esvaziado
close(ch)panicfechapanic
len(ch), cap(ch)0, 0valores no buffer, tamanho do buffervalores restantes, tamanho do buffer

Perguntas frequentes

Qual a diferença entre um channel com buffer e um sem buffer em Go?

Um channel sem buffer (make(chan int)) não tem armazenamento: um envio bloqueia até outra goroutine receber, então todo envio é também uma entrega em mãos e um ponto de sincronização. Um channel com buffer (make(chan int, 3)) guarda até 3 valores; os envios só bloqueiam quando o buffer está cheio e os recebimentos só bloqueiam quando ele está vazio.

O que acontece ao ler de um channel fechado em Go?

Recebimentos em um channel fechado nunca bloqueiam. Primeiro eles esvaziam os valores que ainda estão no buffer, depois devolvem o valor zero do tipo do elemento para sempre. Use v, ok := <-ch para diferenciar: ok é false quando o channel está fechado e vazio. Um laço for v := range ch para nesse ponto.

Quem deve fechar um channel em Go?

Quem envia, e só quando quem recebe precisa saber que não virão mais valores (por exemplo, para encerrar um laço range). Enviar em um channel fechado causa panic, e fechar um channel duas vezes também, então quem recebe fechar o channel cria uma race com quem envia. Você não precisa fechar um channel para liberá-lo; o coletor de lixo recupera channels inalcançáveis de qualquer forma.

O que significa "fatal error: all goroutines are asleep"?

Todas as goroutines do programa estão bloqueadas em uma operação de channel ou em um lock que nada pode completar, então o runtime interrompe o programa. A causa mais comum é enviar em um channel sem buffer no main sem nenhuma outra goroutine recebendo, ou percorrer com range um channel que nunca é fechado.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR