Goroutines e go channels

Programação concorrente com GoLang

Evandro F. Souza
Training Center
8 min readApr 20, 2018

--

Olá, este post faz parte de um série de três partes:

Parte 1: GoLang — Simplificando a complexidade

Parte 2: Goroutines e go channels (você está aqui)

Parte 3: GoLang e Docker

No post anterior, foi descrito uma breve introdução sobre a linguagem do Google, o GoLang. Agora vamos continuar os estudos desta linguagem, o objetivo deste post é ser um tutorial introdutório sobre o uso de Goroutines e Channels — que na minha opinião são a “cereja do bolo” da linguagem. Note que este artigo não pretende ser uma introdução ao Go. Aqui eu suponho que você já tenha conhecimento básico da sintaxe e deseja ter um simples exemplo de uso destas duas funcionalidades únicas da linguagem. Se você quiser aprender o básico primeiro, convido você a dar uma olhada neste ótimo tutorial.

Para demonstrar o uso dos conceitos estudados aqui, vamos criar um web scraper que irá extrair algumas informações dos posts do Medium. Iremos criar o mesmo algoritmo com duas versões: sem e com as goroutines.

Goroutines

Pense em uma goroutine como uma thread, conceito que você provavelmente já está familiarizado com outras linguagens de programação. Em Go, você pode gerar uma nova goroutine para executar código simultaneamente usando a keyword go:

package mainimport (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}

Execute este código clicando aqui.

Se você já estiver familiarizado com a programação concorrente em C, C Sharp ou Java, você pode ficar imediatamente impressionado com a simplicidade comparada a essas linguagens. De fato, gerar uma nova goroutine é muito simples, sendo possível aproveitar o poder da simultaneidade sem uma grande curva de aprendizado. Afinal, conforme é visualizado no exemplo, chamar uma função simultaneamente ocorre com o simples adendo da keyword go.

Para visualizar melhor o funcionamento, copie o código e execute com “go run exemplo.go”, ou através do play.golang.org. Após executado é possível visualizar ambas functions, sem a goroutine( palavra “hello”) e a com goroutine(palavra “world”). Agora faça uma mudança no código, comente a chamada da function sem a goroutine: say(“hello”). Execute novamente e perceba que nada será impresso. Isso ocorre pois a nossa goroutine é independente e desconhecida pela thread principal(ou goroutine principal). Devido a isto, a thread principal termina antes mesmo dos valores serem impressos. Será que existe alguma maneira de solicitar que a thread principal aguarde a goroutine? Vamos dar uma olhada nisso..

Go Channels

Agora que aprendemos como criar novas goroutines concorrentes, é interessante aprender como fazer-las comunicarem entre si. Por exemplo, fazer uma goroutine recém criada avisar a goroutine principal que determinada tarefa já terminou. Para este propósito, vamos usar o go channels:

package mainimport (
“fmt”
“time”
)
func say(s string, done chan string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
done <- “Terminei”
}
func main() {
done := make(chan string)
go say(“world”, done)
fmt.Println(<-done)
}

Execute este código clicando aqui.

E aqui está, criamos um mecanismo de sincronização. A goroutine principal irá aguardar( ou bloquear) até receber uma mensagem da function “say” que está executando em sua própria goroutine.

Os Go Channels podem ser buffered(com buffer) ou unbufferred(sem buffer). Neste post, utilizamos apenas o unbuffered. Isso significa que, para uma goroutine enviar uma mensagem para um channel, outra goroutine deve está esperando para receber esta mensagem. Para um exemplo simples de um channel buffered, leia este exemplo.

Exercicio: Web scraper

Caso você não esteja familiarizado com o termo, em resumo o Web scraping é uma técnica de extração de dados de websites. O aplicativo criado — por vezes chamado de bot ou web crawler — tem como função navegar e extrair dados de websites. Esta técnica já é utilizada há algum tempo, umas das aplicações mais comuns é extrair informações que normalmente não estão disponíveis em API. Porém, é interessante citar outras possíveis aplicações, por exemplo um dos web crawlers mais famosos do mundo é o Googlebot, o indexador de páginas do Google. Recentemente a técnica ganhou ainda mais notoriedade com o advento do Big Data, Data Mining e Machine Learning.

A missão

Desenvolver um simples web scraper. Que receberá como dados de entrada uma lista de posts do Medium( no formato de URL). A aplicação deverá analisar cada post e extrair 3 informações: Título do artigo, nome do autor e quantidade de claps(palminhas). Na versão inicial, será criada uma goroutine para cada URL processada. Se houver 5 posts para serem analisados, o aplicativo deverá criar 5 goroutines.

Existem muitos pacotes que auxiliam no processo de scraper de Go, acredito que muitos deles já utilizam goroutines internamente. Devido a isto, por questões didáticas para este tutorial eu aconselho utilizar o seguintes pacotes:

  • net/http: Cliente de requisições HTTP. Será utilizado para fazer as requisições de cada URL.
  • golang.org/x/net/html: Parser HTML. O HTML retornado de cada url, poderá ser analisado com o auxilio deste parser.

Dica: A técnica de web scraper resume-se em analisar posteriormente o HTML e identificar padrões para extrair os dados. Acesse um post qualquer do Medium e inspecione o HTML, identifique nome de classes CSS, id de elementos ou qualquer outra informação que identifique os elementos que você deseja extrair.

Agora vamos lá! Tente criar este web scraper. Caso precisa configurar o Go no seu ambiente, aconselho este tutorial.

Enquanto você codifica, fique com este Gopher simpático aí. Ele vai te ajudar :)

Uma possível solução

Para ter um bom contraste e demonstrar o quão simples é evoluir o seu software para uma aplicação concorrente. Vamos desenvolver a primeira versão sem goroutines.

Vamos começar pela main function:

A main function possui uma lista de URLs, para cada uma está sendo efetuada uma chamada para a function scrap. A function scrap retorna um struct Result — declarado no cabeçalho do main. A function fmt.Println é chamada, o struct será impresso no formato “{nome do usuário} - {titulo do post} - {quantidade de palmas}”. Por fim, será impresso na tela quantos segundos todas as requisições levaram. Abaixo um exemplo do output da aplicação:

Without goroutines:
Leonardo Carreiro - Why you should use column-indentation to improve your code’s readability - 5.4K claps
Richard Reis - How to think like a programmer — lessons in problem solving - 23K claps
Bill Sourour - Putting comments in code: the good, the bad, and the ugly. - 3.7K claps
Rubens Cantuni - Learning to code (or sort of) will make you a better product designer - 269 claps
(Took 8.397127265 secs)

Nesta execução, o processamento de todas as URLs levou 8 segundos.

Agora vamos dar uma olhada na function scrap, é nela que toda a mágica do web scraper acontece.

Para focar em goroutines e channel, não vou entrar nos detalhes da implementação do scraper. Mas gostaria de aproveitar a ocasião para mostrar alguns detalhes interessantes da linguagem.

Primeiramente, note a linha abaixo:

defer resp.Body.Close()

O defer é um comando muito útil, todo comando rodado com ele, será adiado para executar somente quando a function em si retornar. No caso do exemplo, eu sabia que precisava fechar o Body após consumir o conteúdo dele. Poderia fazer isso logo antes do return. Mas com o defer o código ficou muito mais elegante.

Outro detalhe interessante, note na assinatura da function que o retorno é nomeado ‘r’:

func scrap(url string) (r Result) {
.
.
r.userName = getFirstTextNode(a).Data
.
.
r.title = getFirstTextNode(h1).Data
.
.
r.likes = getFirstTextNode(buttonLikes).Data
return
}

Sendo assim, é possível atribuir valores diretamente ao retorno e no final da function chamar somente return. Sem especificar explicitamente, o Go assume que será retornado o ‘r’.

Para evitar um post muito extenso e cansativo, a definição de algumas functions foram omitidas ( getFirstElementByClass e getFirstTextNode),caso você queira acessar o arquivo completo clique aqui.

Certo, tudo funcionando, mas o nosso código está levando 8 segundos para extrair 4 posts! Isso é muito tempo! agora vamos usar goroutines para extrair estes posts em paralelo. Vamos ver como ficou a main function(thread principal) após a mudança:

Se quiser, volte o post e compare com a versão anterior da function main, é possível perceber que pouca coisa mudou. Agora existe a criação do channel r de tipo Result (aquele struct que declaramos ). A function scrapListURL recebe como parâmetro a lista de URLs e o channel r. A function é executada com o keyword go, logo sabemos que ela será executada dentro de uma goroutine. Para iterar um channel é utilizado o keyword range. Isso é o que acontece com o channel r no trecho abaixo:

for url := range r {  
fmt.Println(url)
}

Desta maneira, enquanto a goroutine estiver executando e o channel não for fechado(close()). O for irá capturar tais valores e imprimir na tela. Ou seja, o channel r está servindo de canal de comunicação entre a goroutine criada e a goroutine principal( ou thread principal).

Para matar a curiosidade, vamos executar e ver quanto tempo leva:

With goroutines:
Leonardo Carreiro - Why you should use column-indentation to improve your code’s readability - 5.4K claps
Richard Reis - How to think like a programmer — lessons in problem solving - 23K claps
Bill Sourour - Putting comments in code: the good, the bad, and the ugly. - 3.7K claps
Rubens Cantuni - Learning to code (or sort of) will make you a better product designer - 269 claps
(Took 2.350607941 secs)

Agora sim! Levou somente 2 segundos ! Agora vamos analisar a function scrapListURL e entender como as URLs são executadas em paralelo:

Certo, primeiramente gostaria de chamar atenção para o trecho abaixo:

func scrapListURL(urlToProcess []string, rchan chan Result) {
defer close(rchan)
var results = []chan Result{}
for i, url := range urlToProcess {
results = append(results, make(chan Result))
go scrapParallel(url, results[i])
}
for i := range results {
for r1 := range results[i] {
rchan <- r1
}
}
}

Nesta function estamos criando um array de channels, ele conterá cada channel que serão responsáveis pela sincronização entre as goroutines novas e a atual. As goroutines novas são da function scrapParallel, note que está function é muito similar a anterior(scrap), a unica diferença entre elas é que agora não há necessidade de um retorno e sim de um parâmetro para receber o channel. Ao final, estamos iterando o array de channels e para cada um retornamos o resultando reencaminhando para o channel da thread principal. A Figura abaixo ilustra como seria a representação visual da nossa aplicação.

Caso queira consultar este código completo, com as versões com e sem goroutine, acesse aqui.

Desafio

Se você chegou até aqui eu lhe agradeço, este post ficou maior que eu esperava. Como agradecimento eu deixo dois desafios para você! O código que escrevemos aqui possui muitos pontos de melhoria, entre eles um dos que mais se destaca é a criação de 1 thread por URL. Da maneira que está, se for processada uma lista de 10.000 URLs, serão criadas dez mil threads paralelas, dependendo da capacidade do computador isso é um problema.

Desafio de goroutines. Modifique a aplicação para processar no máximo 10 URLs em paralelo por vez. Caso seja processada uma lista de 100 URLs, o limite de goroutines processando deve ser 10, as demais deverão aguardar um slot liberar.

Desafio de webscraper. Modifique a aplicação para receber somente o link de alguma comunidade do Medium(por exemplo a Training Center), a aplicação deverá iterar todos os posts desta página e imprimir primeiro aqueles que mais possuírem claps.

No próximo post, vamos pegar esta aplicação e containerizar ela utilizando Docker e Docker Compose.

Se quiser trocar uma ideia ou entrar em contato comigo, pode me achar no Twitter (@e_ferreirasouza) ou Linkedin.

Grande abraço e até a próxima!

--

--