Aula 26 - Tutorial Golang - Channels
Channels
Canais são tubos que conectam
goroutines dentro de seus aplicativos
Go, que permitem a comunicação e posteriormente, a passagem de valores de, e para variáveis.
Eles são incrivelmente úteis e podem ajudá-lo a criar aplicativos de desempenho incrivelmente alto e com muitas rotinas concorrentes em
Go com o mínimo esforço em comparação com outras linguagens de programação.
Isso não foi um acaso, ao projetar a linguagem Go, os principais desenvolvedores decidiram que queriam simultaneidade dentro da linguagem e torná-la o mais simples possível deixando para os desenvolvedores, a liberdade para trabalhar no que é realmente mais importante.
Teoria
A ideia de
canais não é nada nova, pois como muitos dos recursos de simultaneidade, isto é, rotinas que acontecem em paralelo, esses conceitos foram trazidos de nomes como
Hoare’s Communicating Sequential Processes (1978),
CSP para abreviar, e até mesmo de
Guarded Command Language (
GCL)(
1975).
A
Guarded Command Language ( GCL ) é uma linguagem definida por
Edsger Dijkstra para a semântica do transformador de predicado.
Ele combina conceitos de programação de uma forma compacta, antes que o programa seja escrito em alguma linguagem de programação prática.
Sua simplicidade facilita a comprovação da correção de programas, utilizando a
lógica de
Hoare .
Os desenvolvedores do Go, no entanto, têm como missão apresentar esses conceitos da maneira mais simples possível para permitir que os programadores criem aplicativos melhores, mais corretos e altamente simultâneos.
Um Exemplo Simples
Vamos começar com um exemplo simples de como
channels funciona.
Vamos criar uma função que vai calcular um valor aleatório arbitrário e passar de volta para uma variável do tipo
channel chamada
value:
package main
import (
"fmt"
"math/rand"
)
func CalculateValue(values chan int) {
fmt.Println(time.Now().UnixNano())
rand.Seed(time.Now().UnixNano())
value := rand.Intn(10)
fmt.Println("Calculated Random Value: {}", value)
values <- value
}
func main() {
values := make(chan int) // create unbuffered channel of integers
//deferred the closing of our channel until the end of our main() function’s execution
defer close(values)
// start single goroutine passing in our newly created values channel as its paramete
go CalculateValue(values)
// receives a value from our values channel.
value := <-values
fmt.Println(value)
}
Vamos dissecar o código.
Em nossa função
main(), declaramos
values := make(chan int)
Essa declaração cria o canal para que possamos usá-lo posteriormente na goroutine
CalculateValue.
Nota - Usamos
make ao instanciar nosso canal de
values, pois, como
maps e
slices, os
channels devem ser criados antes do uso.
Depois de criarmos o canal, chamamos
defer close(values) que adia o fechamento do nosso canal até o final da execução da função
main().
Essa geralmente é considerada a melhor prática.
Após nossa chamada para adiar, iniciamos nossa única goroutine:
CalculateValue(values) passando nosso canal de
values como parâmetro.
Dentro de nossa função
CalculateValue(), calculamos um único valor aleatório entre
1-10, imprimimos isso e enviamos esse valor para nosso canal de valores chamando
value := <-values.
Após a execução deste código, você deverá ver a saída parecida com esta:
Saída:
Calculated Random Value: {} 7
Exemplo 2
Instanciar e usar canais em seus programas Go parece bastante simples até agora, mas, e em cenários mais complexos?
Unbuffered Channels
Usar um canal tradicional nas goroutines, às vezes pode levar a problemas de comportamento inesperados da aplicação.
Com canais tradicionais sem buffer, sempre que uma goroutine envia um valor para este canal, essa goroutine bloqueará posteriormente até que o valor seja recebido do canal.
Vejamos isso em um exemplo real.
Se dermos uma olhada no código abaixo, é muito semelhante ao código que tínhamos anteriormente.
No entanto, estendemos nossa função
CalculateValue() para executar um
fmt.Println(), depois de enviar seu valor calculado aleatoriamente para o canal.
Em nossa função
main(), adiciona uma segunda chamada ao
CalculateValue(valueChannel), portanto, devemos esperar
2 valores enviados para este canal em uma sucessão muito rápida.
package main
import (
"fmt"
"math/rand"
"time"
)
func CalculateValue(c chan int) {
fmt.Println(time.Now().UnixNano())
rand.Seed(time.Now().UnixNano())
value := rand.Intn(10)
fmt.Println("Calculated Random Value: {}", value)
time.Sleep(1000 * time.Millisecond)
c <- value
fmt.Println("Only Executes after another goroutine performs a receive on the channel")
}
func main() {
valueChannel := make(chan int)
defer close(valueChannel)
go CalculateValue(valueChannel)
go CalculateValue(valueChannel)
values := <-valueChannel
fmt.Println(values)
}
No entanto, quando você executa isso, deve aparecer apenas a instrução final de impressão da nossa primeira goroutines que é realmente executada:
Saída:
Calculated Random Value: {} 1
Calculated Random Value: {} 7
1
Only Executes after another goroutine performs a receive on the channel
A razão para isso é que nossa chamada para
c <- value foi bloqueada em nossa segunda goroutine e posteriormente, a função
main() conclui sua execução antes que nossa segunda goroutine tivesse a chance de concluir sua própria execução.
Buffered Channels
A maneira de contornar esse comportamento de bloqueio é usar algo chamado de
canal em buffer.
Esses
canais em buffer são essencialmente filas de um determinado tamanho que podem ser usadas para comunicação entre
gorotines.
Para criar um canal
com buffer em oposição a um canal
sem buffer, fornecemos um argumento de capacidade para nosso comando make:
bufferedChannel := make(chan int, 3)
Alterando isso para um canal com buffer, nossa operação de envio,
c <- value apenas bloqueia dentro das
goroutines se o canal estiver cheio.
Vamos modificar nosso programa existente para usar um canal em buffer e dar uma olhada na saída.
Observe que foi adicionada uma chamada para
time.Sleep() na parte inferior de nossa função
main() para bloqueio preguiçoso(
lazily block) na função
main(), o suficiente para permitir que nossas
goroutines concluam a execução.
package main
import (
"fmt"
"math/rand"
"time"
)
func CalculateValue(c chan int) {
fmt.Println(time.Now().UnixNano())
rand.Seed(time.Now().UnixNano())
value := rand.Intn(10)
fmt.Println("Calculated Random Value: {}", value)
time.Sleep(1000 * time.Millisecond)
c <- value
fmt.Println("This executes regardless as the send is now non-blocking")
}
func main() {
valueChannel := make(chan int, 2)
defer close(valueChannel)
go CalculateValue(valueChannel)
go CalculateValue(valueChannel)
values := <-valueChannel
fmt.Println(values)
time.Sleep(1000 * time.Millisecond)
}
Agora, quando executamos isso, devemos ver que nossa segunda
goroutine de fato continua sua execução, independentemente do fato de um segundo recebimento não ter sido chamado em nossa função
main().
Graças ao
time.Sleep(), podemos ver claramente a diferença entre
canais sem buffer e
com buffer, e sua natureza de não bloqueio quando não cheios.
Saída:
Calculated Random Value: {} 1
Calculated Random Value: {} 7
7
This executes regardless as the send is now non-blocking
This executes regardless as the send is now non-blocking
É isso pessoal, fico por aqui!
Até mais. :)
Meus links de afiliados:
Obrigado e bons estudos. ;)