DEV Community

Cover image for Um Pouco Sobre Goroutines em Go!
jefferson otoni lima
jefferson otoni lima

Posted on • Updated on

Um Pouco Sobre Goroutines em Go!

Goroutines em vez de threads

Um dos principais objetivos da linguagem de programação Go é tornar a simultaneidade mais simples, rápida e eficiente.

Com certeza, as Goroutines são uma das principais características do Go e são uma das principais razões pelas quais o Go é tão eficiente na execução de tarefas em paralelo.

Quando criamos apis em Go utilizando net/http e recebemos uma porrada de requisições o que o pkg net/http usa? Se sua resposta for Goroutines você está correto 😍.

Eu evito o termo "threads leves", isto gera uma confusão para quem está iniciando ou tentando entender o que são Goroutines. Embora seja verdade que as Goroutines são semelhantes às "threads leves" em outros sistemas, Go usa uma implementação diferente que é otimizada para o contexto do Go. Portanto, pode ser melhor explicar as Goroutines em termos de como elas funcionam no Go que eu acredito que irá facilitar ainda mais o seu entendimento.

Em vez de dizer que as Goroutines são "threads leves", podemos dizer que as Goroutines são unidades leves de concorrência que são executadas em uma única thread de execução ou em várias dependendo de como o runtime Go decide gerenciá-las. Isso significa que o Go pode executar muitas Goroutines simultaneamente em uma única thread ou diversas threads.

As Goroutines são gerenciadas pelo runtime do Go e não pelo sistema operacional subjacente. Isso significa que as Goroutines são muito mais leves e eficientes do que as threads tradicionais do sistema operacional e que o Go pode gerenciá-las de forma mais eficaz para obter o máximo desempenho e escalabilidade.

Um dos objetivos das Goroutines em Go é permitir que você escreva programas concorrentes de maneira fácil e eficiente. Isso significa que você pode ter várias Goroutines em execução simultaneamente, cada uma executando uma tarefa diferente, sem precisar se preocupar com bloqueios de recursos, espera ativa ou outros problemas comuns de programação concorrente.

Para criar uma Goroutine em Go, você pode simplesmente adicionar a palavra-chave "go" antes de uma chamada de função.
Por exemplo, considere o seguinte código:

package main

func myFuncJeff() {
    // seu gode aqui.
}

func main() {
    // inicia a Goroutine
    go myFuncJeff()

    // pode colocar algo aqui
}
Enter fullscreen mode Exit fullscreen mode

Goroutines são leves e tiram proveito de todo o poder de processamento disponível. Goroutines existe apenas no espaço virtual do tempo de execução Go e não no sistema operacional.

Goroutine é um método/função que pode ser executado independentemente junto com outras goroutines. Cada atividade simultânea na linguagem Go é geralmente denominada Goroutine.

Go é uma linguagem multiparadigma e dentre os mais relevantes é o paradigma concorrente. Um dos pontos mais relevantes e importantes na linguagem Go é o trabalho com concorrência, Go inovou ao quebrar o modelo tradicional de threads e sua forma de utilização ao criar um novo modelo, as goroutines. As goroutines são responsáveis por realizar execuções em Go de forma assíncrona. São muito poderosas e uma simples máquina de 1G de Ram e 1CPU poderá subir milhares de goroutines.

Em Go as Goroutines contribuem para tornar a simultaneidade fácil de usar. As goroutines podem ser muito baratas: eles têm pouca sobrecarga além da memória para a pilha, que é apenas alguns kilobytes.

Para tornar as pilhas pequenas, o tempo de execução de Go usa pilhas redimensionáveis e delimitadas. Uma goroutine recém lançada recebe alguns kilobytes, o que é quase sempre suficiente. Quando não é, o tempo de execução aumenta (e encolhe) a memória para armazenar a pilha automaticamente, permitindo que muitos goroutines vivam em uma quantidade modesta de memória.

A sobrecarga da CPU tem em média três instruções baratas por chamada de função. É prático criar centenas de milhares de goroutines no mesmo espaço de endereço. Se goroutines fossem apenas threads, os recursos do sistema acabariam em um número muito menor.

package main

func enviarEmail(...string) {
    // Código para enviar o e-mail
}

func enviarParaFila(mensagem string) {
    // Código enviando para fila
}

func main() {
    // goroutine envia email
    go enviarEmail("jeff@gmail.com", "meu email")

    // goroutine envia para fila
    go enviarParaFila("Mensagem para a fila")

    // Faça alguma outra coisa aqui
}

Enter fullscreen mode Exit fullscreen mode

Neste exemplo, criamos duas funções, enviarEmail e enviarParaFila, que simulam o envio de um e-mail e o envio de uma mensagem para a fila, respectivamente. Em seguida, na função main(), iniciamos duas Goroutines diferentes, uma para enviar um e-mail e outra para enviar uma mensagem para a fila.

Observe que estamos usando a palavra-chave go antes de cada chamada de função para iniciar uma Goroutine separada para cada tarefa. Isso significa que o programa continuará a ser executado normalmente, sem esperar que o envio de e-mail ou a mensagem da fila sejam concluídos.

O design de Go foi fortemente influenciado pelo artigo de C.A.R. Hoare 1978Communicating sequential processes” (Comunicação de processos sequenciais).

Simultaneidade nas ideias do CSP

Um dos modelos mais bem-sucedidos para fornecer suporte linguístico de alto nível para concorrência é o Hoare’s Communicating Sequential Processes, ou CSP, Occam e Erlang são duas línguas bem conhecidas que derivam de CSP. As primitivas de concorrência de Go derivam de uma parte diferente da árvore genealógica cuja principal contribuição é a poderosa noção de canais como objetos de primeira classe. A experiência com várias linguagens anteriores mostraram que o modelo CSP se encaixa bem em uma estrutura de linguagem procedural.

A maior diferença entre Go e o modelo CSP, além da sintaxe, é que o Go modela os canais de comunicação simultânea explicitamente como canais, enquanto os processos da linguagem de Hoare enviam mensagens diretamente uns aos outros, semelhante ao Erlang.

Bem para chegar neste resultado Go novamente sofreu críticas do modelo adotado. Go incorpora uma variante do CSP (Comunicação de processos sequenciais) é uma linguagem formal para descrever padrões de interação em sistemas concorrentes com canais de primeira classe. Não foi adotada uma abordagem de gravação única para valorizar a semântica no contexto da computação concorrente como é feita no Erlang, ao invés disto adotaram algo prático e que resultou em algo poderoso, permitindo programação simultânea simples e segura, mas não proíbe programação incorreta. E o lema criado foi: “Não se comunique compartilhando memória, compartilhe memória comunicando-se”.

A simplicidade e o suporte para concorrências oferecidas por Go gera robustez.

package main

import (
    "fmt"
    "net/http"
    "time"
)

func getSites(url string) {
    resposta, err := http.Get(url)
    if err != nil {
        fmt.Printf("Erro ao ler %s: %s\n", url, err)
        return
    }
    defer resposta.Body.Close()
    fmt.Printf("%s lido com sucesso\n", url)
}

func main() {
    // URLs que iremos ler
    urls := []string{
        "https://www.google.com",
        "https://www.facebook.com",
        "https://www.github.com",
        "https://www.linkedin.com",
    }

    // Goroutine separada para ler cada URL
    for _, url := range urls {
        go getSites(url)
    }

    // Faça alguma outra coisa aqui
        // ...
    time.Sleep(5 * time.Second)
}
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, definimos uma função getSite que usa o pacote net/http para fazer uma solicitação HTTP a um site específico e ler a resposta. Em seguida, na função main(), definimos uma lista de URLs que queremos ler e iniciamos uma Goroutine separada para ler cada URL.

Observe que estamos usando um loop for para percorrer a lista de URLs e iniciar uma Goroutine separada para cada URL usando a palavra-chave go. Isso significa que o programa continuará a ser executado normalmente, sem esperar que cada solicitação HTTP seja concluída.

Para dar tempo às Goroutines para que elas possam fazer o seu trabalho, usamos a função time.Sleep() para suspender a execução do programa por um curto período de tempo. Neste exemplo, usamos um atraso de 5 segundos para permitir que as Goroutines terminem de executar.

Vamos fazer o mesmo código agora usando sync.WaitGroup para sincronizar as nossas Goroutines e ficar ainda melhor o controle sobre elas.

package main

import (
    "fmt"
    "net/http"
    "sync"
)

func getSite(url string, wg *sync.WaitGroup) {
    // Sinalize o WaitGroup quando a Goroutine terminar
    defer wg.Done()

    resp, err := http.Get(url)
    if err != nil {
        fmt.Printf("Erro ao ler %s: %s\n", url, err)
        return
    }
    defer resp.Body.Close()

    fmt.Printf("%s lido com sucesso\n", url)
}

func main() {
    // URLs que iremos ler
    urls := []string{
        "https://www.google.com",
        "https://www.facebook.com",
        "https://www.github.com",
        "https://www.linkedin.com",
    }

    // Crie um WaitGroup 
        // para sincronizar as Goroutines
    var wg sync.WaitGroup

    for _, url := range urls {
        // Adicione WaitGroup para cada Goroutine
        wg.Add(1)

        // Passe o WaitGroup como um ponteiro
        go getSite(url, &wg)
    }

    // Espere até que todas as Goroutines terminem
    wg.Wait()

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

Usando o WaitGroup, podemos garantir que todas as Goroutines terminem de executar antes que o programa continue. Isso é especialmente útil quando queremos que o programa termine apenas quando todas as tarefas simultâneas sejam concluídas.

Concorrência é diferente de Paralelismo

Uma das famosas frases é: Concorrência é sobre lidar com muitas coisas ao mesmo tempo. Paralelismo é fazer muitas coisas ao mesmo tempo”. Concorrência é a composição de cálculos de execução independente. Concorrência é uma maneira de estruturar software, e definitivamente não é paralelismo embora permita o paralelismo.

Se você tiver apenas um núcleo físico em seu processador, seu programa ainda pode ser concorrente, mas não pode ser paralelo. Por outro lado, um programa concorrente bem escrito pode ser executado de forma eficiente em paralelo em processador que possua mais de um núcleo físico. Sugiro dar uma conferida neste vídeo de uma palestra do Rob PikeConcorrência não é Paralelismo”.

Concorrência em Go é muito poderosa e também simples de godar, está foi a intensão dos engenheiros que desenvolveram Go. A resolução de muitos problemas é bem mais eficiente utilizando concorrência e este é o poder de Go, por isto, se tornou um Deus quando o assunto é concorrência. Devido a isto, problemas englobados neste universo serão resolvidos com muita eficiência e o mais importante, com muito pouco recurso computacional.

Quer aprofundar ainda mais sobre concorrência? Só entrar neste link. Quer testar e visualizar alguns exemplos? Click Aqui.

Goroutine é um tópico muito extenso, cheio de detalhes e muitas situações interessantes. Então requer que futuramente façamos um post somente sobre isso para que seja tratado com o devido cuidado e atenção.

Confira um exemplo simples.

package main


import (
    "fmt"
    "time"
)


func main() {


    go func(){fmt.Println("oi sou uma goroutine1!")}()
    go func(){fmt.Println("oi sou uma goroutine2!")}()
    go func(){fmt.Println("oi sou uma goroutine3!")}()
    fmt.Println("Olá, estamos testando as goroutines em Go!")  
    time.Sleep(time.Second)


}
Enter fullscreen mode Exit fullscreen mode

Canais

Os canais em Go são uma forma de sincronizar a comunicação entre Goroutines e compartilhar dados. Eles permitem que uma Goroutine envie dados para outra Goroutine de maneira segura e eficiente, sem a necessidade de usar bloqueios ou semáforos.

Os canais são criados usando a função make() e podem ser usados para enviar ou receber valores de um determinado tipo. Os canais podem ser definidos com uma capacidade opcional, que especifica quantos valores podem ser armazenados no canal antes que ele comece a bloquear. Se a capacidade não for especificada, o canal terá uma capacidade de zero, o que significa que ele pode armazenar apenas um valor por vez.

Para enviar um valor para um canal, usamos a sintaxe <-, como em canal <- valor. Para receber um valor de um canal, usamos a mesma sintaxe, mas invertida, como em valor <- canal. Quando usamos a sintaxe <- sem uma variável à esquerda ou à direita, ele é usado para bloquear a execução da Goroutine até que um valor seja enviado ou recebido pelo canal.

package main

import "fmt"

func produtor(canal chan<- int) {
    for i := 0; i < 10; i++ {
        canal <- i // Envia um valor para o canal
    }
    close(canal) // Fecha o canal
}

func consumidor(canal <-chan int) {
    for valor := range canal { // Itera sobre os valores recebidos do canal
        fmt.Println(valor) // Exibe o valor recebido
    }
}

func main() {
    // Cria um canal sem capacidade definida
    canal := make(chan int)

    // Goroutine do produtor para enviar valores para o canal
    go produtor(canal)

    // Goroutine do consumidor para receber os valores do canal
    consumidor(canal)
}

Enter fullscreen mode Exit fullscreen mode

Neste exemplo, temos duas Goroutines diferentes: uma produtora e uma consumidora. A Goroutine produtora envia valores para o canal, enquanto a Goroutine consumidora os recebe e os exibe na tela.

Observe que estamos usando a palavra-chave chan para definir o tipo do canal. No caso deste exemplo, estamos usando chan int, que significa que estamos criando um canal que pode ser usado para enviar ou receber valores inteiros.

Também usamos as palavras-chave <-chan e chan<- para especificar se o canal deve ser usado para enviar ou receber valores. No caso da Goroutine produtora, estamos usando canal chan<- int, o que significa que estamos criando um canal que pode ser usado apenas para enviar valores inteiros. Na Goroutine consumidora, estamos usando canal <-chan int, o que significa que estamos criando um canal que pode ser usado apenas para receber valores inteiros.

Ao chamar a função close() no canal na Goroutine produtora, estamos sinalizando que não há mais valores para serem enviados pelo canal. Isso faz com que a função range na Goroutine consumidora saia do loop quando todos os valores tiverem sido recebidos.

Vamos reforçar o conceito: Um canal é um objeto de comunicação usado pelas goroutines e que tem o papel fundamental na comunicação entre as goroutines. Tecnicamente, um canal é a transferência de dados no qual os dados podem ser passados ou lidos . Assim, uma goroutine pode enviar dados para um canal, enquanto outras goroutines podem ler os dados do mesmo canal, como uma fila. Os canais é a forma mais segura de comunicação na linguagem Go. Existe outras formas de compartilhar dados em Go, não tão eficientes quanto os canais a equipe que desenvolveu Go decidiram não fechar as possibilidades e é possível compartilhar dados sem usar canais.

Declarando um Canal sem buffer e com buffer


type Promise struct {
  Result chan string
  Error  chan error
}

var (
  ch1  = make(chan *Promise)  // received a pointer from the structure
  ch2  = make(chan string, 1) // allows only 1 channels
  ch3  = make(chan int, 2)    // allows only 2 channels
  ch4  = make(chan float64)   // has not been set can freely receive
  ch5  = make(chan []byte)    // by default the capacity is 0
  ch6  = make(chan bool, 1)   // non-zero capacity
  ch7  = make(chan time.Time, 2)
  ch8  = make(chan struct{}, 2)
  ch9  = make(chan struct{})
  ch10 = make(map[string](chan int)) // map channel
  ch11 = make(chan error)
  ch12 = make(chan error, 2)
  // receives a zero struct
  ch14 <-chan struct{}
  ch15 = make(<-chan bool)          // can only read from
  ch16 = make(chan<- []os.FileInfo) // can only write to// holds another channel as its value
  ch17 = make(chan<- chan bool) // can read and write to
)
Enter fullscreen mode Exit fullscreen mode

Recebendo valores no canal

func main() {

  ch2 <- "okay"
  defer close(ch2)
  fmt.Println(ch2, &ch2, <-ch2)

  ch7 <- time.Now()
  ch7 <- time.Now()
  fmt.Println(ch7, &ch7, <-ch7)
  fmt.Println(ch7, &ch7, <-ch7)
  defer close(ch7)

  ch3 <- 1 // okay
  ch3 <- 2 // okay

  // deadlock // ch3 <- 3 // does not accept any more 
  // values, if you do it will error : deadlockdefer close(ch3)

  fmt.Println(ch3, &ch3, <-ch3)
  fmt.Println(ch3, &ch3, <-ch3)

  ch10["lambda"] = make(chan int, 2)
  ch10["lambda"] <- 100defer close(ch10["lambda"])
  fmt.Println(<-ch10["lambda"])
}
Enter fullscreen mode Exit fullscreen mode

Um pequeno exemplo de goroutine utilizando canais.

Exemplo1:

package main

import "fmt"

func goroutine(c chan string) {
    fmt.Println("Eu sou um canal: " + <-c + "!")
}


func main() {

  fmt.Println("start goroutine!")

  c := make(chan string)

  go goroutine(c)

  c <- "jeffotoni"

}
Enter fullscreen mode Exit fullscreen mode

Exemplo2:

    package main


    import (
        "fmt"
        "time"
    )


    // escrevendo no canal
    func write(ch chan int) {
        for i := 0; i < 5; i++ {
            ch <- i
            fmt.Println("escrever:", i, "to ch")
        }
        close(ch)
    }


    func main() {


        // channel com buffer
        ch := make(chan int, 2)


        // goroutine
        go write(ch)


        //aguarde um pouco
        time.Sleep(1 * time.Second)


        // listando o canal
        for v := range ch {
            fmt.Println("ler", v, "from ch")
            time.Sleep(2 * time.Second)
        }
    }
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, estamos definindo uma variável chamada ch que é um canal que só pode ser usado para receber valores do tipo struct{}. Usamos a palavra-chave <-chan para especificar que esse canal só pode ser usado para receber valores.

Confira o código abaixo:

package main

import "fmt"

func main() {
    // Cria um canal sem capacidade definida
    ch := make(chan struct{})

    // Inicia uma Goroutine para enviar um valor para o canal
    go func() {
        ch <- struct{}{} // Envia um valor para o canal
    }()

    // Recebe um valor do canal e exibe uma mensagem
    <-ch
    fmt.Println("Valor recebido do canal")
}

Enter fullscreen mode Exit fullscreen mode

Select

O select é uma estrutura de controle em Go que permite que você monitore vários canais simultaneamente e execute a primeira operação que estiver pronta. Isso permite que você escreva código conciso e eficiente que responde a eventos de vários canais ao mesmo tempo.

package main

import (
    "fmt"
    "time"
)

func enviarMesg(canal chan<- string, mensagem string, atraso time.Duration) {
    // Espera por um período de tempo aleatório
    time.Sleep(atraso)

    // Envia a mensagem para o canal
    canal <- mensagem
}

func main() {
    // Cria dois canais sem capacidade definida
    canal1 := make(chan string)
    canal2 := make(chan string)

    // Inicia duas Goroutines separadas para enviar mensagens para os canais
    go enviarMesg(canal1, "Mensagem para o canal 1", time.Second*2)
    go enviarMesg(canal2, "Mensagem para o canal 2", time.Second*1)

    // Use o select para receber a primeira mensagem que estiver pronta
    select {
    case msg1 := <-canal1:
        fmt.Println("Mensagem recebida do canal 1:", msg1)
    case msg2 := <-canal2:
        fmt.Println("Mensagem recebida do canal 2:", msg2)
    }
}

Enter fullscreen mode Exit fullscreen mode

Neste exemplo, temos duas Goroutines diferentes que enviam mensagens para dois canais diferentes. Usando o select, estamos monitorando os dois canais e exibimos a primeira mensagem que chega em um deles.

Observe que estamos usando a sintaxe <- para receber valores dos canais dentro dos casos do select. O select aguardará até que um dos casos esteja pronto, o que significa que uma mensagem foi recebida de um dos canais.

Além disso, estamos usando o time.Sleep() na função enviarMsg() para simular um atraso aleatório antes de enviar a mensagem para o canal. Isso é feito para demonstrar que a mensagem enviada ao canal mais rapidamente pode não ser a primeira a ser recebida devido aos atrasos.

O select é uma ferramenta poderosa em Go que pode ser usada para escrever código eficiente que monitora vários canais ao mesmo tempo. É particularmente útil para lidar com eventos assíncronos e permitir que o programa execute várias tarefas simultaneamente.

Espero ter colaborado para melhor entendimento quando o assunto é Goroutine.

Um outro assunto complementar e importante é "Workloads: CPU-Bound e IO-Bound" recomendo a leitura para entender ainda mais.

Aqui você irá encontrar mais exemplos:
manual Go em: gobootcamp.jeffotoni
Exemplos diversos de Goroutines em: goexample

Em Go teremos um arsenal para trabalhar com goroutines e gerenciar concorrências mas sem perder a simplicidade, legibilidade e produtividade.

Go é amor

Top comments (0)