Um channel em Go passa valores entre goroutines e faz uma esperar pela outra. Aprenda channels com e sem buffer, fechamento, deadlocks, channels nil e como o select espera em vários ao mesmo tempo.
Uma goroutine sozinha não consegue devolver um resultado. Ela não tem um valor de retorno que você possa pegar. A resposta de Go é o channel: um cano com tipo, em que uma goroutine envia e outra recebe.
Este post trata de como criar channels, da diferença entre channels sem buffer e com buffer, de fechamento, do erro de deadlock que você vai encontrar cedo ou tarde, e do select para esperar em vários channels ao mesmo tempo. Todo programa abaixo rodou no Go 1.26, e a saída foi colada da execução.
Criar um channel, enviar e receber
Um channel em Go é criado com make, e o tipo dele é chan T, em que T é o tipo de valor que ele carrega. O operador de seta <- faz os dois trabalhos: ch <- v envia v, e <-ch recebe um valor.
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
ch <- "hello from a goroutine"
}()
msg := <-ch
fmt.Println(msg)
fmt.Printf("%T\n", ch)
}
Ele imprime:
hello from a goroutine
chan string
A goroutine envia uma string, e main a recebe. Não tem sleep nem loop verificando se a goroutine terminou. <-ch simplesmente espera até um valor chegar. Essa espera é a outra metade do que um channel faz.
Tipos de channel só de envio e só de recebimento
Um parâmetro de função pode prometer usar um channel em uma direção só. chan<- int é um channel em que você só pode enviar. <-chan int é um de onde você só pode receber. A seta mostra para que lado os valores correm em relação a chan.
package main
import "fmt"
func produce(out chan<- int) {
for i := range 3 {
out <- i * i
}
close(out)
}
func consume(in <-chan int) int {
total := 0
for v := range in {
total += v
}
return total
}
func main() {
ch := make(chan int)
go produce(ch)
fmt.Println(consume(ch))
}
Ele imprime:
5
main passa um chan int comum para as duas funções, e Go converte para o tipo mais restrito em cada chamada. produce envia 0, 1 e 4, e consume soma os três. close e for range ganham uma seção própria mais abaixo.
O tipo mais restrito é verificado pelo compilador. Se consume tenta enviar, o build para:
package main
func consume(in <-chan int) {
in <- 1
}
func main() {
ch := make(chan int)
go consume(ch)
<-ch
}
O build falha com:
./main.go:4:2: invalid operation: cannot send to receive-only channel <-chan int in (variable of type <-chan int)
A mensagem cita o tipo duas vezes, o que soa estranho, mas a ideia é clara. Tipos com direção não custam nada em tempo de execução, e o compilador os garante, então use-os sempre que uma função só envia ou só recebe.
Channels sem buffer e com buffer
Um channel criado com make(chan int) é sem buffer: ele não tem espaço para guardar um valor. Um channel criado com make(chan int, 2) tem buffer, com espaço para dois. Essa única diferença decide quando um envio precisa esperar.
- Em um channel sem buffer, um envio espera até outra goroutine receber, e um recebimento espera até outra goroutine enviar. As duas se encontram, o valor passa, e as duas seguem em frente. Esse encontro costuma ser chamado de rendezvous.
- Em um channel com buffer, um envio só espera quando o buffer está cheio, e um recebimento só espera quando ele está vazio.
Veja os dois antes de ler o programa por trás deles:
Um channel sem buffer em cima e um channel com buffer de capacidade 2 embaixo. O envio sem buffer espera até um receptor pegar o valor. Os envios de 1 e 2 com buffer terminam na hora porque há espaços livres, o envio de 3 espera porque a prateleira está cheia, e ele passa assim que um receptor pega o 1 e libera um espaço.
Aqui estão esses passos em palavras, caso a animação não rode para você:
- No channel sem buffer, o emissor oferece 1. Ninguém está recebendo, então o emissor fica bloqueado.
- Um receptor chega e pega o 1. O envio e o recebimento terminam juntos, e as duas goroutines seguem em frente.
- No channel com buffer de capacidade 2, o emissor envia 1 e depois 2. Os dois vão para espaços livres, então nenhum envio espera.
lenagora é 2. - O emissor tenta enviar 3. Os dois espaços estão cheios, então esse envio fica bloqueado.
- Um receptor pega o 1, o valor da frente. O 2 avança, e um espaço fica livre.
- O 3 entra no espaço livre, o envio bloqueado termina, e o emissor segue em frente.
Este programa faz os mesmos passos, com uma goroutine como emissor e main como receptor. Só main imprime, e cada print vem depois de uma operação no channel que já precisa ter acontecido, então a ordem da saída não muda entre execuções:
package main
import "fmt"
func main() {
// Unbuffered: the hatch has no room, so a send waits for a receiver.
hatch := make(chan int)
sent := make(chan bool)
go func() {
hatch <- 1 // waits here until main receives
sent <- true
}()
fmt.Println("hatch: received", <-hatch)
<-sent
fmt.Println("hatch: the sender has moved on")
// Buffered: the shelf has 2 slots, so 2 sends finish with nobody receiving.
shelf := make(chan int, 2)
ready := make(chan bool)
done := make(chan bool)
go func() {
shelf <- 1
shelf <- 2
ready <- true
shelf <- 3 // the shelf is full, so this waits
done <- true
}()
<-ready
fmt.Println("shelf: len", len(shelf), "cap", cap(shelf))
fmt.Println("shelf: received", <-shelf)
<-done
fmt.Println("shelf: the send of 3 has finished, len", len(shelf))
fmt.Println("shelf: received", <-shelf)
fmt.Println("shelf: received", <-shelf)
}
Ele imprime:
hatch: received 1
hatch: the sender has moved on
shelf: len 2 cap 2
shelf: received 1
shelf: the send of 3 has finished, len 2
shelf: received 2
shelf: received 3
Na primeira metade, o emissor não consegue chegar a sent <- true antes de main pegar o 1. Na segunda metade, a goroutine chega a ready <- true enquanto ninguém está recebendo de shelf, então os dois primeiros envios não esperaram. O envio de 3 só termina depois que main pega um valor. Os valores saem na ordem em que entraram, porque um channel é first in, first out.
cap é o tamanho do buffer que você passou para make, e len é quantos valores estão esperando agora. Um channel sem buffer tem os dois em 0. Mas não tome decisões com base em len em código de verdade. Quando você for agir, outra goroutine pode já ter mudado o valor.
Explicado como se você tivesse dez anos
Imagine a cozinha de um restaurante com dois cozinheiros. Um prepara a comida, e o outro coloca os pratos nas bandejas.
Um channel sem buffer é uma pequena janela na parede entre os dois. Ela não tem prateleira. O primeiro cozinheiro segura um prato na janela e não pode soltar até o segundo pegar do outro lado. Se o segundo estiver ocupado, o primeiro fica ali parado segurando o prato. Quando o prato troca de mãos, os dois voltam ao trabalho.
Um channel com buffer é uma prateleira com um número fixo de espaços, digamos dois. O primeiro cozinheiro pode deixar um prato e ir embora, desde que haja um espaço vazio. Quando os dois espaços estão cheios, ele precisa ficar esperando um espaço liberar. O segundo cozinheiro pega os pratos da frente da prateleira. Se a prateleira está vazia, o segundo cozinheiro espera.
A versão precisa
Um channel é uma fila com uma trava dentro, compartilhada por toda goroutine que o tem. Um channel sem buffer tem uma fila de tamanho zero. Um envio nele só termina quando um receptor pega o valor diretamente, então as duas goroutines com certeza se encontram. Depois que um envio sem buffer retorna, você sabe que o receptor tem o valor.
Um channel com buffer de capacidade N guarda até N valores. Um envio só bloqueia quando já há N valores esperando, e um recebimento só bloqueia quando não há nenhum. Depois que um envio com buffer retorna, você só sabe que o valor está no buffer. Talvez ninguém tenha recebido ainda.
“Bloqueia” quer dizer que a goroutine está estacionada. Ela não fica girando nem usando CPU. O runtime a acorda quando o channel pode avançar.
Onde a analogia falha: uma prateleira de cozinha aceita qualquer tipo de prato, mas um channel carrega exatamente um tipo. Vários cozinheiros podem esperar na mesma janela, e a linguagem não promete qual deles vai primeiro. E a maior diferença aparece na seção sobre fechamento: uma janela fechada não fica trancada. Ela continua entregando pratos vazios para sempre.
Deadlock: quando toda goroutine está esperando
Um envio em um channel sem buffer, sem nenhuma outra goroutine para receber, espera para sempre. Se toda goroutine do programa está presa assim, Go percebe e para o programa:
package main
import "fmt"
func main() {
ch := make(chan int)
ch <- 1
fmt.Println(<-ch)
}
Ele imprime o erro e um stack trace, e depois para:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main tentou enviar, e a única goroutine que poderia receber é o próprio main, que agora está preso no envio. O recebimento da linha seguinte nunca roda. O trace mostra o que a goroutine estava fazendo quando parou: [chan send].
Trocar a primeira linha por make(chan int, 1) faz esse programa funcionar, porque o valor cabe no buffer. Em código de verdade, porém, adicionar um buffer para sumir com um deadlock costuma esconder um problema de design.
Isso é um fatal error, não um panic, então recover não consegue capturá-lo. E a verificação só dispara quando toda goroutine está bloqueada. Se uma goroutine fica presa em um channel para sempre enquanto outras continuam rodando, como as goroutines de um servidor HTTP, Go não diz nada. Isso é um vazamento de goroutine, e você só vai notar como uma memória que não para de crescer.
Fechar um channel
Um emissor chama close(ch) para dizer “não vêm mais valores”. Os receptores ainda podem pegar os valores que já estão no buffer. Depois disso, todo recebimento retorna na hora com o zero value do tipo. A forma com dois valores diz qual dos dois você recebeu:
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 10
close(ch)
v, ok := <-ch
fmt.Println(v, ok)
v, ok = <-ch
fmt.Println(v, ok)
v, ok = <-ch
fmt.Println(v, ok)
}
Ele imprime:
10 true
0 false
0 false
O 10 foi enviado antes do close, então o primeiro recebimento ainda o pega, com ok true. O channel fica então fechado e vazio, e todo recebimento seguinte dá 0, false na hora. Ele não bloqueia e não para. Esses são os “pratos vazios para sempre” que a analogia prometeu. É a mesma ideia de comma-ok de quando você busca uma chave em um map.
Verificar ok na mão cansa, então for v := range ch faz isso por você. O loop recebe valores até o channel estar fechado e vazio, e aí termina:
package main
import "fmt"
func main() {
words := make(chan string)
go func() {
for _, w := range []string{"flour", "eggs", "milk"} {
words <- w
}
close(words)
}()
for w := range words {
fmt.Println("got", w)
}
fmt.Println("the channel is closed, so the loop ended")
}
Ele imprime:
got flour
got eggs
got milk
the channel is closed, so the loop ended
Se a goroutine esquecesse o close(words), o loop esperaria uma quarta palavra para sempre, e o programa terminaria com o erro de deadlock da seção anterior.
Só o emissor fecha
Enviar em um channel fechado é um panic, e fechar um channel duas vezes também:
package main
import "fmt"
func main() {
ch := make(chan int, 1)
close(ch)
fmt.Println("closed")
ch <- 1
}
Ele imprime a primeira linha, e depois para:
closed
panic: send on closed channel
Fechar duas vezes dá panic: close of closed channel. Um receptor não tem como saber se um emissor está prestes a enviar, então um receptor que fecha um channel corre o risco desse panic. A regra que te protege é simples: só o emissor fecha, e só quando não tem mais nada para enviar. Com vários emissores, nenhum deles deve fechar. Quem fecha é algo que sabe que todos terminaram. A parte sobre sync mostra a ferramenta de costume para isso.
Você também não precisa fechar todo channel. Um channel não é um arquivo. O garbage collector limpa um channel inalcançável, fechado ou não. Feche um channel quando os receptores precisam saber que os valores acabaram, como o range precisa.
Channels nil bloqueiam para sempre
O zero value de um tipo channel é nil, e var ch chan int te dá um. Enviar para um channel nil ou receber dele bloqueia para sempre:
package main
import "fmt"
func main() {
var ch chan int
fmt.Println(ch == nil)
<-ch
}
Ele imprime true, e depois o erro de deadlock:
true
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive (nil chan)]:
O trace até diz (nil chan), o que ajuda quando você esqueceu um make. Fechar um channel nil também dá panic.
Isso parece só uma armadilha, mas tem um bom uso. Em um select, um case em um channel nil nunca está pronto, então atribuir nil a uma variável de channel desliga esse case. Você vai ver isso na seção sobre juntar dois channels.
select: esperar em vários channels
Uma instrução select espera até uma de várias operações de channel poder seguir, e então roda esse case. Ela parece um switch, mas todo case é um envio ou um recebimento:
package main
import "fmt"
func split(nums []int, evens, odds chan<- int, done chan<- bool) {
for _, n := range nums {
if n%2 == 0 {
evens <- n
} else {
odds <- n
}
}
done <- true
}
func main() {
evens := make(chan int)
odds := make(chan int)
done := make(chan bool)
go split([]int{4, 7, 1, 8}, evens, odds, done)
for {
select {
case n := <-evens:
fmt.Println("even:", n)
case n := <-odds:
fmt.Println("odd:", n)
case <-done:
fmt.Println("done")
return
}
}
}
Ele imprime:
even: 4
odd: 7
odd: 1
even: 8
done
split envia cada número em um de dois channels sem buffer, e depois sinaliza done. Cada envio espera até main receber, então só um case fica pronto por vez, e a saída mantém a ordem da entrada.
Quando vários cases estão prontos
Se mais de um case está pronto quando o select olha, Go escolhe um deles ao acaso, com a mesma chance para cada. Ele não testa de cima para baixo como um switch. Isso é de propósito. Se o select sempre preferisse o primeiro case, um primeiro channel movimentado poderia deixar os outros sem vez para sempre.
O programa acima imprime em ordem fixa só porque nunca tem dois cases prontos ao mesmo tempo. Se split usasse channels com buffer, vários números poderiam estar esperando juntos, e a ordem da saída mudaria entre execuções. Então não conte com a ordem dos cases para dar prioridade. Se um channel realmente precisa ganhar, verifique-o antes em um select próprio, ou desenhe o fluxo para que ele não concorra.
default: não esperar nada
Um select com um case default nunca bloqueia. Se nenhum outro case está pronto agora, o default roda no lugar. Isso te dá um envio ou recebimento que só acontece se puder acontecer imediatamente:
package main
import "fmt"
func trySend(ch chan<- int, v int) {
select {
case ch <- v:
fmt.Println("sent", v)
default:
fmt.Println("full, skipped", v)
}
}
func tryReceive(ch <-chan int) {
select {
case v := <-ch:
fmt.Println("received", v)
default:
fmt.Println("empty, nothing to receive")
}
}
func main() {
shelf := make(chan int, 2)
trySend(shelf, 1)
trySend(shelf, 2)
trySend(shelf, 3)
tryReceive(shelf)
tryReceive(shelf)
tryReceive(shelf)
}
Ele imprime:
sent 1
sent 2
full, skipped 3
received 1
received 2
empty, nothing to receive
É a prateleira da animação, mas o envio de 3 não espera. Ele desiste e cai no default. Você usaria isso para descartar uma métrica em vez de deixar uma requisição mais lenta.
Cuidado com default dentro de um loop. Um loop em volta de um select com default nunca bloqueia, então ele gira e queima um núcleo inteiro de CPU enquanto espera algo acontecer.
Um timeout com time.After
time.After(d) retorna um channel que recebe um valor quando d passar. Coloque-o em um select ao lado do trabalho de verdade, e o que ficar pronto primeiro ganha:
package main
import (
"fmt"
"time"
)
func fetch(delay time.Duration) <-chan string {
out := make(chan string, 1)
go func() {
time.Sleep(delay)
out <- "report ready"
}()
return out
}
func wait(result <-chan string, limit time.Duration) {
select {
case r := <-result:
fmt.Println(r)
case <-time.After(limit):
fmt.Println("gave up waiting")
}
}
func main() {
wait(fetch(0), time.Second)
wait(fetch(time.Second), 10*time.Millisecond)
}
Ele imprime:
report ready
gave up waiting
O primeiro trabalho termina na hora contra um limite de um segundo. O segundo leva um segundo contra um limite de 10 milissegundos. As diferenças são grandes de propósito, para o resultado ser o mesmo em uma máquina lenta.
Vale saber dois detalhes. fetch usa um buffer de 1, então a goroutine ainda consegue enviar o resultado atrasado depois que wait desistiu, e aí sair. Com um channel sem buffer, ela ficaria bloqueada para sempre em um envio que ninguém vai receber. E desde o Go 1.23, um timer de time.After que nada mais referencia é limpo pelo garbage collector mesmo que nunca tenha disparado. Conselhos antigos avisavam que time.After em um loop vaza timers. Isso não é mais verdade.
Para timeouts que atravessam várias chamadas de função, como uma requisição HTTP que chama um banco de dados, use context, visto na parte sobre context e padrões de concorrência.
Juntar dois channels, com nil para desligar um case
Receber de dois channels até os dois estarem fechados é onde um channel nil mostra seu valor. Quando um channel fecha, atribua nil à variável dele, e o select para de escolher esse case:
package main
import (
"fmt"
"slices"
)
func send(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func main() {
a := send(1, 2, 3)
b := send(10, 20)
var got []int
for a != nil || b != nil {
select {
case n, ok := <-a:
if !ok {
a = nil // a nil channel is never ready, so this case is now off
continue
}
got = append(got, n)
case n, ok := <-b:
if !ok {
b = nil
continue
}
got = append(got, n)
}
}
slices.Sort(got)
fmt.Println(got)
}
Ele imprime:
[1 2 3 10 20]
Sem a = nil, um a fechado estaria pronto a cada volta do loop, entregando 0, false sem parar, e o loop ficaria girando. Com ele, o case nunca mais pode ser escolhido. O loop termina quando os dois são nil. Os valores de a e b chegam em uma ordem que muda entre execuções, então o programa os ordena antes de imprimir.
Dois padrões para começar
Boa parte da concorrência em Go é construída com funções pequenas que retornam channels. Vale aprender dois agora. Worker pools e pipelines se apoiam neles na parte sobre context e padrões de concorrência.
Um generator retorna um channel só de recebimento
Um generator é uma função que inicia uma goroutine, retorna um <-chan T e envia valores nele:
package main
import "fmt"
func countdown(from int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for i := from; i > 0; i-- {
out <- i
}
}()
return out
}
func main() {
for n := range countdown(3) {
fmt.Println(n)
}
fmt.Println("liftoff")
}
Ele imprime:
3
2
1
liftoff
O tipo de retorno <-chan int significa que quem chama só pode receber, então só a goroutine lá dentro pode enviar ou fechar. defer close(out) garante que o channel feche de qualquer jeito que a goroutine termine, e é isso que deixa quem chama usar range.
Um channel done para uma goroutine
Um generator que nunca termina precisa de um jeito de receber a ordem de parar, ou a goroutine dele espera para sempre em um envio que ninguém recebe. O sinal de costume é um channel done que quem chama fecha:
package main
import "fmt"
func naturals(done <-chan struct{}) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for i := 1; ; i++ {
select {
case out <- i:
case <-done:
return
}
}
}()
return out
}
func main() {
done := make(chan struct{})
nums := naturals(done)
for n := range nums {
fmt.Println(n)
if n == 3 {
break
}
}
close(done)
for range nums {
// drain until the generator closes out
}
fmt.Println("the generator has stopped")
}
Ele imprime:
1
2
3
the generator has stopped
A goroutine espera duas coisas ao mesmo tempo: alguém receber o próximo número, ou done ser fechado. Depois que main sai do loop com break, ele fecha done. Um channel fechado está sempre pronto para recebimento, então a goroutine retorna e fecha out. O segundo loop só termina quando out está fechado, o que prova que a goroutine realmente parou.
Aqui, fechar é o sinal certo, não enviar um valor. Um envio acorda um receptor. Um close acorda todos os receptores, não importa quantas goroutines estejam ouvindo. O tipo de elemento struct{} não ocupa memória e diz que o valor não importa, só o evento.
Compartilhe memória comunicando
O conselho de Go é “não comunique compartilhando memória; compartilhe memória comunicando”. Em palavras simples: em vez de várias goroutines lendo e escrevendo a mesma variável e revezando com uma trava, passe os dados por um channel para que uma goroutine seja dona deles de cada vez. O emissor entrega um valor e para de mexer nele, e o receptor vira o dono. Ninguém mais o tem, então não há disputa. Isso não proíbe travas. Um mutex é mais simples para um contador ou um cache, e a parte sobre sync cobre esses casos. Use channels quando os dados passam de uma etapa do trabalho para a próxima, ou quando goroutines precisam sinalizar umas às outras.
O que lembrar
make(chan T)é sem buffer: um envio espera até um receptor pegar o valor.make(chan T, n)guarda aténvalores, e os envios só esperam quando ele está cheio.- Use
chan<- Te<-chan Tnas assinaturas de função. Assim o compilador impede um receptor de enviar. - Se toda goroutine está bloqueada, Go para com
all goroutines are asleep - deadlock!. Se só algumas estão, nada te avisa. - Só o emissor fecha. Um channel fechado dá o zero value e
ok == falsepara sempre, orangepara no close, e enviar em um channel fechado dá panic. - Um channel nil bloqueia para sempre. Em um
select, atribuirnila um channel desliga o case dele. selectespera em vários channels e escolhe ao acaso entre os cases prontos.defaulto torna não bloqueante, etime.Afterdá a ele um timeout.- Fechar um channel
doneavisa toda goroutine que está ouvindo para parar.
Um envio em um channel sem buffer só termina quando alguém tem o valor.