DEV Community

Igor Melo
Igor Melo

Posted on • Updated on

Golang: Desmistificando channels - Conceito e sintaxe

Introdução

Resolvi fazer esse texto porque no meu primeiro contato com channels eu não entendi muito bem como funcionava, daí eu sempre fugia para o WaitGroup ou fazia alguma Mutex para usar goroutines, mesmo quando a melhor solução seria usar channels.

Fiz esse texto com o objetivo de deixar mais claro como usar channels e os problemas comuns que você vai encontrar ao usá-los de forma errada.

O que é e para que serve?

Channel, ou canal, é uma forma nativa do Go para comunicar e sincronizar goroutines.

O channel já é thread-safe por padrão, ou seja, enviar e receber são operações seguras de se usar com goroutines, então você não vai precisar se preocupar em criar mutexes nem nada do tipo.

Com channels você consegue facilmente fazer coisas como:

  • Coletar o resultado de várias goroutines fazendo requisições HTTP e ir mostrando na tela
  • Criar workers que processam no máximo 50 operações concorrentes
  • Fazer outras goroutines esperarem por um sinal que sua aplicação iniciou

Criando e usando channels

Para criar um channel, fazemos:

func main() {
    ch := make(chan int)
}
Enter fullscreen mode Exit fullscreen mode

O channel pode ser basicamente de qualquer tipo: int, string, struct{} e outros.

Esse tipo é o tipo de dado que vai ser enviado e recebido pelo canal.
Detalhe que ao enviar uma informação para um channel, o que o outro lado recebe é uma cópia do dado.

Dessa forma você já evita vários problemas de data race, porque a goroutine que recebeu o dado pode ler e modificar sem o risco de afetar outra goroutine.
A exceção para isso é quando você cria channels de tipos que não copiam o dado inteiro, como ponteiros, maps e slices.

Exemplos de channels que vão copiar somente a referência, e não o dado completo:

ch := make(chan *int)
ch := make(chan []string)
ch := make(chan map[string]User)
Enter fullscreen mode Exit fullscreen mode

Para enviar informações para um channel, fazemos:

func main() {
    ch := make(chan string)

    ch <- "Shakespeare"
}
Enter fullscreen mode Exit fullscreen mode

Para receber do channel, fazemos:

func main() {
    ch := make(chan string)

    // recebendo de um channel e descartando valor
    <-ch

    // recebendo de um channel e guardando numa variável
    name := <-ch
}
Enter fullscreen mode Exit fullscreen mode

Por que não funciona?

Se você rodar qualquer um dos códigos do exemplo anterior, vai acontecer isso:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        main.go:5 +0x31
exit status 2
Enter fullscreen mode Exit fullscreen mode

Isso acontece porque os channels por padrão vão pausar a execução da goroutine que tentar enviar ou receber. Ou seja, a goroutine que tentar enviar vai esperar até que outra esteja pronta para receber, e da mesma forma a goroutine que tentar receber vai esperar até que outra envie.

No caso acima quando eu tento enviar para um channel, eu pauso a goroutine main para esperar que outra goroutine leia o que enviei, mas como não tem outra goroutine rodando, a main vai ficar esperando “para sempre” (deadlock).

O runtime do Go percebe que esse programa vai ficar parado indeterminadamente, porque todas as goroutines estão “dormindo” (all goroutines are asleep), então ele encerra o programa com um código de erro.

O mesmo acontece no segundo exemplo.

Também existem os buffered channels, que permitem a gente enviar para um canal uma quantidade X de dados antes que ele comece a bloquear a goroutine, mas vamos ver isso mais para frente.

Usando channels corretamente

Para corrigir o problema do deadlock anterior, vamos criar outra goroutine que lê do channel, enquanto a goroutine main vai enviar para o channel.

package main

import (
    "fmt"
)

func main() {
    ch := make(chan int)

    // obs. 1: 'go' vai fazer com que essa função seja executada numa 
    // nova goroutine, então ler do channel não vai bloquear a main

    go func() {
        // obs. 2: eu não preciso passar o ch como parâmetro da função
        // porque o ch está no da main escopo
        <-ch
    }()

    // a goroutine main vai esperar até que consiga enviar
    ch <- 12

    fmt.Println("Fim")
}
Enter fullscreen mode Exit fullscreen mode

Se você está confuso com ch <- 12 e <-ch, você pode pensar que a seta aponta para onde o dado está fluindo.

// o dado está saindo do channel ch (receber) e sendo descartado
<-ch

// o dado está saindo do channel ch (receber) e sendo atribuído à variável x
x := <-ch

// o dado 12 está indo para o channel ch (enviar)
ch <- 12
Enter fullscreen mode Exit fullscreen mode

Conclusão

Isso conclui a parte 1 dessa série, com o conceito básico de channels.
Na parte 2 vamos ver um exemplo mais realístico de onde e como seria utilizado channels.

Top comments (2)

Collapse
 
valdirmendesdev profile image
Valdir Mendes

Parabéns pela iniciativa! Já conheço um pouquinho de channels, mas sempre vale a pena revisar e aprender coisas novas!

Collapse
 
igormelo profile image
Igor Melo

Obrigado, Valdir!
É bem importante pra mim saber se a didática está funcionando