Aula 26 – Tutorial Golang – Channels
Aula 26 – Tutorial Golang – Channels
Página principal do blog
Todas as aulas desse curso
Aula 25 Aula 27
Redes Sociais:
Meus links de afiliados:
Hostinger
Digital Ocean
One.com
Melhore seu NETWORKING
https://digitalinnovation.one/
Participe de comunidades de desenvolvedores:
Fiquem a vontade para me adicionar ao linkedin.
E também para me seguir no https://github.com/toticavalcanti.
Código final da aula:
https://github.com/toticavalcanti
Canais do Youtube:
Toti
Backing Track / Play-Along
Código Fluente
Putz!
Vocal Techniques and Exercises
PIX para doações
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