- Golang – 01. Introdução
- Golang – 02. Tipos Básicos
- Golang – 03. Structs e Funções e Métodos
- Golang – 04. Estruturas de Controle
- Golang – 05. Gerenciando Pacotes
- Golang – 06. Biblioteca Padrão I – fmt e strings
- Golang – 07. Biblioteca Padrão II – os, os/exec e os/user
- Golang – 08. Interface
- Golang – 09. Goroutines – Concorrência e Paralelismo
- Golang – 10. Goroutines – Canais
- Golang – 11. Pacotes e Documentação no Go
Índice
- 1. Canal
- 1.1. Exemplo 01: iniciando um canal
- 1.1. Escrevendo e Lendo em um Canal
- 1.2. Canal sem Buffer
- 1.2.1. Exemplo 02: canal sem buffer
- 1.3. Canal com Buffer
- 1.3.1. Exemplo 03: canal com buffer
- 1.4. Laço for-range e fechando o Canal
- 1.4.1. Exemplo 04: for-range
- 1.5. Select – case
- 1.5.1. Exemplo 05: select-case
- 2. Goroutines e Boas Práticas
- 2.1. Canal Done
- 2.1.1. Exemplo 06: canal done
- 2.1.2. Exemplo 07: canal done para aprimorar código connectdb.go
- 2.2. Como os Canais se Comportam
- 2.2.1. Exemplo 08: Desabilitando cases em um select-case
- 3. Considerações Finais
Canais os meios através dos quais as goroutines se comunicam umas com as outras, trocando dados tipados. Neste artigo será apresentado como os canais funcionam e como empregá-los.
Canal
Um canal é o meio pelo qual se pode enviar e receber dados tipados para as goroutines. Sua declaração pode ser feita através da palavra chave var
seguida do nome do canal, da palavra chave chan
e o tipo do canal, mas neste caso uma posterior inicialização deve ser feita com a palavra chave make
O código a seguir ilustra a inicialização do canal e como este á percebido pelo Go:
Exemplo 01: iniciando um canal
package main import "fmt" func main() { var ch chan int fmt.Printf("ch é nil? %v\nTipo: (%T)\nValor: %v\n\n", ch == nil, ch, ch) ch = make(chan int) fmt.Printf("ch é nil? %v\nTipo: (%T)\nValor: %v", ch == nil, ch, ch) }
A saída deste código é apresentada a seguir:
ch é nil? true
Tipo: (chan int)
Valor: <nil>
ch é nil? false
Tipo: (chan int)
Valor: 0xc00001c1e0
A linha 6 cria o canal ch
para transportar inteiros, que neste instante está vazio, valor nil
, como mostra a saída da linha 7. Na linha 9 o canal é instanciado passando a não conter mais um valor nil
e sim um ponteiro para um inteiro, como mostra a saída da linha 10.
Em geral a declaração de um canal é feita junto com a sua inicialização com o operador “:=
“, em uma única linha como segue:
ch := make(chan int)
Como visto na saída do código anterior os canais possuem como valor um ponteiro para o tipo associado, o que simplifica a passagem de um canal de escrita para uma função, já que como um ponteiro este pode ser escrito e lido diretamente no bloco da função.
Escrevendo e Lendo em um Canal
Para escrever e ler um canal se usa o operador “<-
” de forma semelhante ao operador de associação, ou seja:
ch <- v
sendo ch
um canal e v
um valor, esta linha escreve o valor de v
no canal ch
. De forma semelhante
v <-ch
a variável v
recebe o valor do canal ch
. É possível declarar a variável v
com o operador “:=
” e receber o valor do canal na mesma linha como segue:
v := <-ch
Ao passar um canal como parâmetro para uma função, este pode ser passado como:
(ch <-chan int)
– para indicar quech
será um canal de leitura na função;(ch chan<- int)
– para indicar quech
será um canal de escrita na função;(ch chan int)
– para indicar quech
será um canal de leitura ou de escrita (bidirecional) na função.
Em geral um canal é unidirecional, suportando apenas operações de escrita ou de leitura pela função. Embora seja possível implementar um canal bidirecional geralmente não é o mais adequado.
Canal sem Buffer
Os canais criados nos exemplos acima são canais unbuffered, ou seja sem buffer. Cada escrita em um canal unbuffer faz com que a goroutine pause até que outra goroutine leia este conteúdo do canal. Da mesma forma a uma leitura de um canal unbuffer aberto faz com que a goroutine pause até que outra goroutine escreva no canal. Isto significa que não se pode escrever ou ler em um canal sem que duas goroutine estejam em execução simultaneamente.
Observe que a função main
é a goroutine principal de todas as goroutines em um código Go.
O código a seguir mostra um canal unbuffer operando entre a goroutine main()
e a função print()
Exemplo 02: canal sem buffer
package main import ( "fmt" ) func print(id int, c <-chan int) { fmt.Printf("(%d) channel: %v\n", id, <-c) } func main() { ch := make(chan int) go print(1, ch) go print(2, ch) go print(3, ch) ch <- 0 ch <- 1 ch <- 2 }
A função print()
, linhas 7 à 9, será chamada como uma goroutine três vezes seguidas, linhas 14 à 16. Para deixar mais claro o que está acontecendo, em tempo de execução, a função print
possui dois parâmetros: uma id
para identificar a ordem de chamada da função; e o canal c
, como apenas leitura, por onde ela recebe as entradas enviadas pela função main
, nas linhas 18 à 20.
Executando este código a saída é algo como:
(3) channel: 0
(1) channel: 1
(2) channel: 2
Quando este código é executado são criadas três threads para as goroutines, linhas 14 à 16, as quais param a execução e ficam aguardando que algo seja escrito no canal c
, linha 8. Como o canal ch
, na função main
, inicia a ser escrito na linha 18, a thread em execução na sequência é a que primeiro lê o conteúdo do canal ch
, neste caso foi a terceira goroutine iniciada.
A ordem em que as goroutines são executadas não é, necessariamente, a mesma que elas foram invocadas na função main()
. Isto é algo esperado em código executado em concorrência, sua execução ocorre de acordo com a disponibilidade do sistema, sendo adiada até o sistema disponibilize os recursos necessários e que exista algo a processar.
Após a segunda escrita no canal ch
, linha 19, a primeira goroutine responde e por último a segunda responde à última escrita ao canal ch
, linha 20.
Em tempo de execução três goroutines são inicializadas criando três threads no sistema que aguardam a escrita no canal ch
. De acordo com a disponibilidade do sistema as goroutines vão sendo atendidas, o que permite que o programa adiante a escrita no canal ch
da linha 18, para a 19 e por fim para a 20.
Canal com Buffer
Canal com buffer é declarado com o make
, adicionando uma vírgula e o tamanho do buffer ao final da declaração, com abaixo:
ch := make(chan int, 20)
Esta linha cria um canal de capacidade de 20 elementos inteiros, em ch
. Canais com buffer possuem um número limitado de escritas antes de bloquear a execução. Neste caso, se o buffer estiver cheio uma escrita subsequente pausa o canal até que alguma goroutine leia algum elementos do canal. De forma semelhante, se o buffer estiver vazio o canal pausa até que alguma goroutine escreva no canal.
As funções internas len
e cap
, quando aplicadas a um canal, retornam a sua capacidade e a quantidade de elementos no canal.
O código a seguir, como está escrito, emprega um canal unbuffered, a declaração com buffer está comentada na linha 19 neste primeiro momento.
Exemplo 03: canal com buffer
package main import ( "fmt" "sync" ) var wg sync.WaitGroup func print(ch <-chan int) { defer wg.Done() for i := 0; i < 6; i++ { fmt.Printf("Print %02d (len: %d, loop: %d)\n", <-ch, len(ch), i) } } func main() { ch := make(chan int) // ch := make(chan int, 3) wg.Add(1) go print(ch) for i := 0; i < 6; i++ { value := i * 10 fmt.Printf("Write %02d\n", value) ch <- value } fmt.Println("Wait...") wg.Wait() }
Execute o código sem buffer o que deve gerar a saída como:
a: Write 00
b: Print 00 (len: 0, loop: 0)
c: Write 10
d: Write 20
e: Print 10 (len: 0, loop: 1)
f: Print 20 (len: 0, loop: 2)
g: Write 30
h: Write 40
i: Print 30 (len: 0, loop: 3)
j: Print 40 (len: 0, loop: 4)
k: Write 50
l: Wait...
m: Print 50 (len: 0, loop: 5)
As linhas de saída foram manualmente nomeadas com as letras de “a” à “m” para facilitar o apontamento das saídas impressas. Antes de iniciar a discussão do código é importante observar que um canal unbuffered sempre possui comprimento 0, mesmo que a função len
fosse chamada antes do canal ser lido na linha 13 o resultado ainda seria zero.
Quando o código inicia sua execução a goroutine, iniciada na linha 22, pausa a sua execução aguardando que o canal ch
seja escrito, o que ocorre apenas no laço for
da linha 24 à 28.
Este laço escreve o valor do contador i*10
por seis vezes no canal ch
, abastecendo o canal para ser lido pela goroutine. Como o “Write 00
“, linha a da saída, é impresso antes de escrever no canal ch
, ele acaba sendo escrito antes na tela. Desta forma a ordem de execução das linhas seriam:
- linha 26, imprime o “
Write 00
“; - linha 27, escreve um 0 no canal
ch
; - linha 13, na goroutine, que estava aguardando a escrita no canal imprime a saída b, “Print 00 (len: 0, loop: 0)”;
- o laço
for
prossegue a execução parando a goroutine novamente na linha 13, aguardando uma nova escrita no canal; - na sequência a linha c é impressa, “
Write 10
“, e um 10 é escrito no canalch
e enquanto este é lido na goroutine e preparado a impressão, o laço da funçãomain
executa uma segunda impressão, “Write 20
” a linha d, e escreve um 20 no canalch
.
Isto gera as duas impressões seguidas “Write 10
” e “Write 20
” que gera a impressão de que o 10 e o 20 foram escritos num canal unbuffered, o que não ocorre. Esta dupla impressão ocorre porque a escrita no canalch
corre na linha 27, antes da escrita na tela das mensagens “Write xx
“. - as linhas de saída e e f ocorre por um efeito semelhante ao descrito acima, mas agora na linha de impressão da goroutine, linha 13. Enquanto a função
main
aguarda que o canal seja liberado para escrever o 20 a mensagem “Print 10 (len: 0, loop: 1)
” é impressa na goroutine. Isto libera o canal para a escrita do 20 e a goroutine executa o segundo laço e imprime “Print 20 (len: 0, loop: 2)
“; - na sequência os efeitos dos itens 5 e 6 se repetem;
- por fim um 50 é escrito no canal, antes de ser escrito a saída k, e um “
Wait...
” é impresso, indicando que o programa está aguardando umwg.Done()
, para encerrar, o que ocorre na execução do último laço da goroutine; - isto gerar a impressão da linha m e o programa termina.
Agora veja como trabalha o código com a buffer habilitado. Para isto comente e linha 18 e remova o comentário da linha 19. Com isto o canal ch
terá um buffer para três inteiros. Execute novamente o código e veja a saída seguinte:
Write 00
Write 10
Write 20
Write 30
Print 00 (len: 3, loop: 0)
Print 10 (len: 2, loop: 1)
Print 20 (len: 1, loop: 2)
Print 30 (len: 0, loop: 3)
Write 40
Write 50
Wait...
Print 40 (len: 1, loop: 4)
Print 50 (len: 0, loop: 5)
Observe que correm quatro escritas “Write
” (00
, 10
, 20
e 30
) antes que a goroutine consiga imprimir o primeiro “Print ...
“. Da mesma forma que descrita anteriormente, o “Write 30
” é escrito e o programa pausa na linha 27 aguardando que algum dado seja lido do canal ch
, para que o “30
” possa ser escrito. Isto ocorre quando o “00
” é lido do canal, na função print
, o que encadeia a impressão das quatro linhas “Print ...
” pela goroutine, “Print 00
” a “Print 30
“.
As saídas len()
mostram o canal sendo esvaziado, de 3 elementos até 0, vazio. A função len()
ocorre após a leitura do canal, observe a linha 13:
fmt.Printf("Print %02d (len: %d, loop: %d)\n", <-ch, len(ch), i)
Enquanto o canal é lido pelo “<-ch
“, na linha 13, o elemento 30
é escrito na linha 27. Por isto que o len(ch)
retorna um comprimento de 3 e não 2.
Para deixar mais claro experimento trocar a linha 13 pela linha abaixo:
fmt.Printf("Print %02[2]d (len: %[1]d/%[3]d, loop: %[4]d)\n", len(ch), <-ch, len(ch), i)
Os índices [1] a [4] se referem aos elementos len(ch)
, <-ch,
len(ch)
e i
, passados ao fmt.Printf
para alterar a ordem da escrita dos elementos. Com esta mudança o comprimento do canal é lido antes e depois do canal ser lido. Execute novamente e observe a saída:
Write 00
Write 10
Write 20
Write 30
Print 00 (len: 3/3, loop: 0)
Print 10 (len: 3/2, loop: 1)
Print 20 (len: 2/1, loop: 2)
Print 30 (len: 1/0, loop: 3)
Write 40
Write 50
Wait...
Print 40 (len: 0/1, loop: 4)
Print 50 (len: 1/0, loop: 5)
No primeiro “Print
” o comprimento do canal é 3 antes e depois da leitura do canal, “len: 3/3
“. Isto aconteceu porque enquanto no canal é lido o “00
“, linha 13, um “30
“, linha 27, é escrito no canal dando a impressão que o número de elementos no canal permaneceu constante em 3.
Em geral canais unbuffered atendem a maioria das demandas, no entanto canais com buffer tem o seu lugar e podem ser bem convenientes quando se sabe quantos elementos se podem acumular em um buffer antes de pausar a execução do código.
Laço for-range e fechando o Canal
Um laço for-range
pode ser empregado para ler todos os elementos passados por um canal aberto. O laço se repete até que um close(channel)
seja executado fechando o canal.
No código abaixo um for-range
é empregado para alterar o código anterior e ler os elementos do canal. Um close(ch)
foi adicionado na linha 29 para fechar o canal e terminar o for-range
.
Exemplo 04: for-range
package main import ( "fmt" "sync" ) var wg sync.WaitGroup func print(ch <-chan int) { defer wg.Done() for c := range ch { fmt.Printf("Print %02d (len: %d)\n", c, len(ch)) } } func main() { // ch := make(chan int) ch := make(chan int, 3) wg.Add(1) go print(ch) for i := 0; i < 6; i++ { value := i * 10 fmt.Printf("Write %02d\n", value) ch <- value } close(ch) fmt.Println("Wait...") wg.Wait() }
Agora a leitura do canal é feita no início do laço for-range
e por isto não faz mais sentido a alteração da linha 13 feita anteriormente. Por isto esta linha foi restaurada ao original para uma impressão mais limpa.
Executando o código a saída será:
Write 00
Write 10
Write 20
Write 30
Print 00 (len: 3)
Print 10 (len: 2)
Print 20 (len: 1)
Print 30 (len: 0)
Write 40
Write 50
Wait...
Print 40 (len: 1)
Print 50 (len: 0)
O efeito na saída foi exatamente o mesmo do código anterior.
Um canal fechado não pode mais ser escrito ou mesmo fechado novamente ou gera um erro de panic
. No entanto um canal fechado pode ser lido e sempre retorna um zero. Isto levanta a questão:
Como saber se o retorno “
0
” é um retorno real ou o retorno de um canal fechado?
Quando se lê um canal ele também retorna um booleano que indica se o canal está aberto (true) ou fechado (false). Este booleano de status pode ser lido adicionando uma variável separada por vírgula na leitura do canal:
value, ok := <- ch
A variável ok
vai retornar o status do canal, aberto ou fechado, e quando omitido apenas o valor do canal é lido.
Não é a forma mais elegante, mas a função print
, do código anterior, pode ser substituída pelo código abaixo, para empregar a leitura do status do canal em um laço for
infinito:
... func print(ch <-chan int) { defer wg.Done() for { c, ok := <-ch if !ok { break } fmt.Printf("Print %02d (len: %d)\n", c, len(ch)) } } ...
Este código vai gerar a mesma saída do código anterior, no entanto agora o laço termina quando a variável ok
retornar um false, acionando o break
da linha 7, que termina o laço.
Select – case
A sentença select
do Go é parecida com a sentença switch
com respeito a sua sintaxe:
select {
case <cond 1>:
// cond 1 ...
case <cond 2>:
// cond 2 ...
...
default:
// default:
}
No entanto as semelhanças não vão muito além. No bloco select
o case
deve ser sempre sobre um canal recebendo ou escrevendo algo ou gera um erro de InvalidSelectCase
.
Outra diferença fundamental é sobre o comportamento, num switch
a execução passa uma única vez pelos diferentes case
s saindo na primeira condição satisfeita ou alcançando o final do bloco, ou seja, um bloco switch
não passa de uma forma elegante de fazer uma sequencia de if-else
. Já num bloco select
a execução do código é bloqueada até que um dos seus case
s esteja pronto, recebendo ou escrevendo algo em um canal, para então sair do bloco.
No caso do uso da condição default
o select
acaba por funcionar de forma muito semelhante ao switch
, e geralmente seu emprego não é algo desejado.
Para ilustrar uma aplicação, suponha que se deseja acessar a um banco de dados distribuído em cinco servidores e somente o primeiro a atender será aberto. O código a seguir implementa a ideia:
Exemplo 05: select-case
package main import ( "fmt" "math/rand" "time" ) func connectDBase(server string, ch chan<- string) { fmt.Printf("Try connection in %s...\n", server) time.Sleep(time.Microsecond * (20 + time.Duration(rand.Int63n(10)))) ch <- fmt.Sprintf("%s connected!", server) } func main() { serverList := []string{"Newton", "Einstein", "Plank", "Fermi"} var c []chan string rand.Seed(time.Now().UnixNano()) for i, server := range serverList { c = append(c, make(chan string)) go connectDBase(server, c[i]) } select { case s := <-c[0]: fmt.Println(s) case s := <-c[1]: fmt.Println(s) case s := <-c[2]: fmt.Println(s) case s := <-c[3]: fmt.Println(s) } }
A função connectDBase
, linhas 9 à 13, simula a conexão ao banco de dados retornando no canal ch
uma mensagem da conexão realizada, linha 12. O time.Sleep
, linha 11, apenas é para simular o tempo de resposta do servidor.
Na linha 16 é declarada uma lista de nomes dos servidores e na linha 17 uma lista de canais para responder aos servidores. O laço das linhas 21 à 24 adiciona um novo canal na lista c
, linha 22, e em seguida chama a goroutine para fazer a conexão, linha 23.
Por fim um select
, linhas 26 à 35, aguarda até que algum servidor responda e termine o código. Uma possível saída deste código é apresentada a seguir:
Try connection in Fermi...
Try connection in Newton...
Try connection in Einstein...
Try connection in Plank...
Einstein connected!
Mesmo que diversos servidores respondam simultaneamente apenas um dos case
do select
será acionado. No caso de acionamento simultâneo apenas um dos cases
será aleatoriamente acionado.
Goroutines e Boas Práticas
Embora o código anterior sirva para ilustrar o funcionamento do select
ele apresenta um problema grave o qual se deve ter atenção ao empregar concorrência e paralelismos em seu código: as goroutines que não responderam a tempo continuam rodando mesmo após um dos canais ter respondido. Como se trata de um exemplo todas as goroutines são finalizadas ao atingir o final da função main
. No entanto em um programa funcional após conectar ao banco de dados alguns processos devem ser realizados, o que não terminaria o programa de imediato.
Da forma que o código está desenhado as goroutines vão continuar rodando consumindo recursos de memória e processamento da máquina desnecessariamente, o que pode deixar o aplicativo mais lento e ineficiente.
Por isto uma boa prática é sempre gerenciar bem o emprego das goroutines, garantindo que estas sejam terminadas sempre que não forem mais necessárias, e que os canais envolvidos tenham sidos exauridos (com todos os elementos devidamente descarregados) e fechados.
Canal Done
É de praxe criar um canal done
para comunicar às goroutines que o seu processamento não é mais necessário e desabilitá-la. Em geral este canal é do tipo struct
vazia, mas poderia ser de qualquer outro tipo.
Antes de corrigir o código connectdb.go
um problema mais simples será implementado para ilustrar o uso do done
. O código a seguir emprega uma goroutine como um gerador da série de Fibonacci e um done
é usado para parar a execução deste gerador.
Exemplo 06: canal done
package main import ( "fmt" "math/rand" "sync" "time" ) var wg sync.WaitGroup func fibo(ch chan<- int, done <-chan struct{}) { defer wg.Done() a, b := 0, 1 for { c := a + b a, b = b, c select { case ch <- c: case <-done: fmt.Println("\nFibo stoped...") close(ch) return } } } func main() { ch := make(chan int) done := make(chan struct{}) wg.Add(1) go fibo(ch, done) rand.Seed(time.Now().UnixMicro()) max := 5 + rand.Intn(15) fmt.Printf("\nFirst %d Fibonacci numbers:\n0 1", max+2) for i := 0; i < max; i++ { fb := <-ch fmt.Print(" ", fb) } done <- struct{}{} wg.Wait() }
A função para gerar a série de Fibonacci é definida na função fibo
, linhas 12 à 27. Cada novo número da série gerado é passado para o canal ch
na linha 20, do select-case
. Juntamente o canal done
é verificado para saber se o sinal de cancelamento foi passado, case da linha 21. Portanto o select
aguarda até que algum dos case
s tenham sucesso: alguém lê o canal ch
ou um struct{}
vazia é passada no done
. Quando o done
é passado o canal ch
é fechado e a função fibo
é encerrada.
A função main
cria dois canais sendo o ch
para receber os elementos da série de Fibonacci e o done
, para finalizar a função fibo
. Na linha 34 a função fibo
é executada como uma goroutine, acionando o gerador de Fibonacci.
As linhas 36 e 37 iniciam o gerador de números aleatórios e o número máximo de elementos a serem gerados como sendo algo entre 5 e 15 elementos. Os dois primeiros elementos da série (0 e 1) são impressos na linha 39.
No laço for
os elementos da série de Fibonacci, gerados pela goroutine fibo
, são lidos do canal ch
, linha 41, e impressos na sequência.
Ao atingir o número de elementos o laço for
termina, o done
recebe um struct{}
, linha 45, e é acionado o final da execução da função fibo
, com a impressão da mensagem “Fibo stoped...
“, linha 22.
O WaitGroup
, linhas 10, 13, 33 e 46, foram inseridos aqui apenas para dar tempo para que a mensagem final “Fibo stoped...
” possa ser impressa antes que a função main
termine.
A saída do código acima é apresentado a seguir:
First 21 Fibonacci numbers:
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765
Fibo stoped...
No próximo código um canal done
é empregado para aprimorar o código do connectdb.go
, do exemplo 05.
Exemplo 07: canal done para aprimorar código connectdb.go
package main import ( "fmt" "math/rand" "sync" "time" ) var wg sync.WaitGroup func connectDBase(server string, ch chan<- string, done <-chan struct{}) { defer wg.Done() fmt.Printf("Try connection in %s...\n", server) time.Sleep(time.Microsecond * (20 + time.Duration(rand.Int63n(10)))) select { case ch <- server: return case <-done: fmt.Printf("Server %s close!\n", server) return } } func main() { serverList := []string{"Newton", "Einstein", "Plank", "Fermi"} ch := make(chan string) done := make(chan struct{}, len(serverList)) rand.Seed(time.Now().UnixNano()) for _, s := range serverList { wg.Add(1) go connectDBase(s, ch, done) } doneAll := func() { for i := 0; i < len(serverList); i++ { done <- struct{}{} } } server := <-ch doneAll() fmt.Printf("Server %s CONNECTED.\n", server) wg.Wait() close(ch) }
A primeira alteração foi no select-case
que foi migrado para a função connectDBase
, linhas 16 à 22, onde fica aguardando que o canal ch
seja lido por outra goroutine, neste caso a função main
, ou que o done
seja escrita para acionar o final da função.
Como no código anterior um WaitGroup
(linhas 10, 13, 33 e 47) foi adicionado para garantir que as chamadas às goroutines tenham retornado.
Na função main
o slice de canais foi reduzido para apenas um canal ch
, linha 27, visto que apenas um servidor deve responder a tempo os demais não necessitam mais escrever no canal. Apenas se deve tomar cuidado de não fechar o canal ch
até que as goroutines tenham retornado, o que é feito posicionando o close(ch)
após o wg.Wait()
, linha 48. De outra forma o código pode gerar um erro de “panic: close of closed channel
“, por tentar escrever em um canal fechado.
O canal done
foi criado com um buffer do comprimento igual ao número de servidores, elementos na lista serverList
, para que cada chamada das goroutines, o que ocorre no for
das linhas 32 à 35, possam ser cancelados.
A função doneAll
, declarada nas linhas 37 à 41, apenas carrega o buffer do canal done
com uma struct vazia quando acionado.
Quando o código é executado ele irá para na linha 43 e aguardar até que algum servidor responda, passando seu nome para a variável server
. Depois disto a função doneAll
é chamada e desencadeia o fechamento das goroutines. Na linha 46 uma mensagem é impressa com o servidor conectado. Na linha 47 é aguardado que todas as goroutines terminem para finalmente fechar o canal ch
, linha 48.
Executando este código deve ser gerado uma saída semelhente a:
alves@arabel:../connectdb2$ go run connectdb.go
Try connection in Fermi…
Try connection in Einstein…
Try connection in Plank…
Try connection in Newton…
Server Newton close!
Server Fermi close!
Server Einstein CONNECTED.
Server Plank close!
Como os Canais se Comportam
Os canais possuem comportamentos diferentes sobre tentativas de escrita, leitura ou fechamento dependente de seu status, aberto ou fechado. Mesmo um canal nil
responde de forma diferenciada. Estes comportamentos são fundamentais para se compreender a resposta de um código nas diferentes situações, bem como suas mensagens de erro.
A tabela a seguir mostra as possíveis respostas de um canal buffered, unbuffered e nil
com respeito a solicitações de leitura, escrita e fechamento.
Canal | Leitura | Escrita | Fechamento |
---|---|---|---|
Unbuffered aberto | pausa até algo ser escrito | pausa até algo ser lido | fecha normalmente |
Unbuffered fechado | retorna valor zero (use ", ok" para checar se está fechado). | PANIC | PANIC |
Buffered aberto | pausa se o buffer estiver vazio | pausa se o buffer estiver cheio | fecha normalmente, mas mantém os valores em buffer |
Buffered fechado | retorna os valores restantes no buffer. Se o buffer estiver vazio retorna zero (use ", ok" para checar se está fechado). | PANIC | PANIC |
Nil | para eternamente | para eternamente | PANIC |
Em especial observe o comportamento de um canal nil
quanto a escrita e leitura. Este comportamento pode ser empregado para desabilitar canais em um select-case
, visto que estes não respondem a uma tentativa de escrita ou leitura.
O código a seguir mostra uma forma simples de desabilitar case
s em um select-case
.
Exemplo 08: Desabilitando cases em um select-case
package main import ( "fmt" "time" ) func selectCase(ch1, ch2, ch3 chan<- int, done <-chan struct{}) { for { select { case ch1 <- 1: ch1 = nil continue case ch2 <- 2: ch2 = nil continue case ch3 <- 3: ch3 = nil continue case <-done: fmt.Println("Goroutine finised...") return } // more code here... } } func main() { ch1 := make(chan int) ch2 := make(chan int) ch3 := make(chan int) done := make(chan struct{}) go selectCase(ch1, ch2, ch3, done) fmt.Printf("ch%d is nil\n", <-ch1) fmt.Printf("ch%d is nil\n", <-ch3) fmt.Printf("ch%d is nil\n", <-ch2) done <- struct{}{} time.Sleep(time.Microsecond * 100) }
Em linhas gerais a função selectCase
opera sobre os canais ch1
, ch2
e ch3
, desabilitando-os quando estes recebem uma escrita, já o canal done
é empregado para encerrar a goroutine. Os continue
s, colocados aqui após fazer os canais igual a nil
, são desnecessários a menos que algum código seja inserido dentro do laço for
, antes ou depois do select-case
.
Uma vez executado o código, no primeiro laço do for
da linha 9, o select-case
, linhas 10 à 23, para a execução do programa na goroutine selectCase
até que algo seja lido dos canais ch1
, ch2
ou ch3
, ou que algo seja escrito no canal done
.
A linha 36 lê o canal ch1
e Isto aciona o case da linha 11 o qual envia 1 para o canal e o desabilita fazendo-o igual a nil
. Deste ponto em diante o ch1
não consegue mais ser escrito ou lido. Ainda na linha 36, uma mensagem de que o canal é nil
é impressa.
Na linha 36 o mesmo se repete para o canal ch3
e depois, na linha 37, para o canal ch2
. Por fim done
é acionado para desabilitar a goroutine. O time.Sleep
tem o único objetivo de aguarda para que a goroutine consiga desligar, embora seja desnecessário uma vez que o programa termina logo em seguida.
Considerações Finais
O uso de canais é algo fundamental códigos concorrentes e embora possam muitas vezes ser substituídos por WaitGroup, Once, Mutex, … em geral seu emprego deixa o código simples e direto. Não estendi a discussão mas tudo que o pacote sync implementa pode ser feito com canais.
Em canais está a alma do código concorrente e do emprego eficiente de máquinas com muito núcleos, portanto é muito importante a sua compreensão e domínio.