Alura > Cursos de Programação > Cursos de GoLang > Conteúdos de GoLang > Primeiras aulas do curso Go: use concorrência para otimizar sua aplicação

Go: use concorrência para otimizar sua aplicação

Concorrência em Go - Apresentação

Olá, eu sou o instrutor Guilherme Lima e estou feliz que você queira aprender mais sobre concorrência aplicada na linguagem Go.

Audiodescrição: Guilherme se identifica como um homem branco. Possui cabelos curtos e pretos, além de bigode e barba rentes ao rosto, na mesma cor. No rosto, usa óculos de armação preta. No corpo, veste uma camisa azul clara com gola. Ao fundo, uma parede lisa com um quadro de desenho geométrico pendurado é iluminada em tons de verde e azul.

Conhecendo o projeto

Neste curso, vamos criar um projeto do zero, inspirado no mundo real, onde temos uma aplicação que busca preços de diferentes sites. Como nosso objetivo é a concorrência, não vamos criar o scrap (raspagem de dados) para realizar, de fato, a busca desses preços. Vamos simular essa busca e desenvolver uma aplicação que busca em cada um dos sites de maneira sequencial.

O que aprenderemos?

Para o cenário acima, perceberemos que podemos usar concorrência para obter um ganho de performance considerável. Vamos entender também:

Por questões de tempo, não abordaremos neste curso os tópicos de Atomics, Select, Mutex e outros assuntos que também envolvem concorrência com Go. O assunto de concorrência é muito amplo e extenso. Portanto, decidimos falar de maneira profunda sobre canais e desenvolver um projeto prático que você construirá do zero, aplicará concorrência e verá o ganho de performance que obtemos ao utilizá-la na aplicação.

Conclusão

Sabemos que a concorrência não resolverá todos os problemas de código, mas temos certeza de que ela pode melhorar bastante a performance em situações que enfrentamos no mundo real.

Concorrência em Go - Projeto inicial

Vamos iniciar nossos estudos sobre concorrência em Go. Para começar, não teremos um projeto base com várias coisas já feitas. Vamos começar do zero, focando exclusivamente em concorrência.

Para isso, criaremos um projeto vazio. Na área de trabalho do computador, criaremos uma nova pasta chamada "buscador". A ideia é criar uma aplicação que simula a busca de preços de produtos em diferentes e-commerces e plataformas.

Configurando o ambiente de desenvolvimento

Utilizaremos o Visual Studio Code para editar nosso código. Ao abri-lo, pressionaremos "Ctrl + J" para abrir o terminal e iniciaremos o projeto com o comando abaixo, onde buscador será o nome do projeto:

go mod init buscador

O VS Code abrirá um novo arquivo chamado go.mod, dentro do qual nos informará o nome do nosso módulo e a versão do Go que estamos utilizando.

go.mod:

module buscador

go 1.22.5

Podemos fechar esse arquivo, clicando no "X" à direita do seu nome, na parte superior do programa.

Estruturando a aplicação

Vamos criar a estrutura da nossa aplicação. Acessando a aba do explorador de arquivos, na lateral esquerda, clicaremos no botão "New Folder" — representado por uma pasta e o símbolo de "+" — para criar uma pasta chamada "cmd". Mantendo a pasta selecionada, clicaremos no botão "New File" — representado por uma folha e o símbolo de "+" — para criar um arquivo chamado main.go.

Esses conceitos já foram abordados em cursos anteriores de Go.

Acessando o arquivo main.go, adicionaremos o pacote main e a função main por meio do atalho pkgm.

pkmg

main.go:

package main

func main() {

}

Este é o ponto de partida de toda a nossa aplicação.

Voltando ao explorador lateral, criaremos uma pasta chamada "internal" no mesmo nível da "cmd". Dentro dela, criaremos outra pasta chamada "fetcher" para manter todo o código relacionado à busca de preços das outras aplicações. Dentro da pasta "Fetcher", criaremos um arquivo chamado price_fetcher.go.

Acessando esse arquivo, informaremos o pacote fetcher.

price_fetcher.go:

package fetcher

Simulando a busca de preços

A ideia agora é simular essa busca. Não iremos, de fato, acessar lojas para consultar preços, pois o curso é sobre concorrência, não sobre scraping. Queremos simular essas buscas e pensar em quanto tempo elas levariam para aplicar concorrência de forma prática.

Nosso objetivo principal nesta função é buscar preços de diferentes sites. Criaremos uma função que simula a busca do primeiro site, chamada FetchPriceFromSite1(). Ao realizar a busca, a informação mais importante é o preço, que será retornado como float64.

package fetcher

func FetchPricesFromSite1() float64 {

}

Entre as chaves da função, simularemos a autenticação, o acesso, busca e retorno da API, e depois coletaremos o preço. Isso levará um tempo, que simularemos com time.Sleep(), demorando um segundo no site 1. Após um segundo, retornaremos um valor aleatório rand.Float64() * 100.

func FetchPriceFromSite1() float64 {
    time.Sleep(1 * time.Second)
    return rand.Float64() * 100
}

Cada e-commerce pode levar um tempo diferente para retornar. Vamos criar funções semelhantes para os sites e 3, alterando o tempo de espera para 3 e 2 segundos, respectivamente.

func FetchPriceFromSite2() float64 {
    time.Sleep(3 * time.Second)
    return rand.Float64() * 100
}

func FetchPriceFromSite3() float64 {
    time.Sleep(2 * time.Second)
    return rand.Float64() * 100
}

Integrando as funções ao main

Temos três funções, cada uma levando um tempo diferente para retornar um valor. Nosso próximo desafio é usar essas funções na função main().

Acessando o arquivo main.go, entre as chaves da função main(), criaremos e inicializaremos as variáveis price1, price2 e price3 para armazenar os preços retornados de suas respectivas funções.

Em seguida, exibiremos esses valores na tela usando fmt.Printf(), formatando para duas casas decimais e pulando uma linha entre cada preço para melhor visualização, por meio do comando "R$ %.2f \n":

main.go:

package main

func main() {
    price1 := fetcher.FetchPriceFromSite1()
    price2 := fetcher.FetchPriceFromSite2()
    price3 := fetcher.FetchPriceFromSite3()
    
    fmt.Printf("R$ %.2f \n", price1)
    fmt.Printf("R$ %.2f \n", price2)
    fmt.Printf("R$ %.2f \n", price3)
}

Executaremos o programa no terminal com o comando abaixo:

go run cmd/main.go

Ele buscará o preço de cada site e exibirá os resultados.

R$ 59.42

R$ 55.45

R$ 96.23

Medindo o tempo de execução

Queremos descobrir quanto tempo levamos para executar essa busca. Para isso, voltaremos ao arquivo main.go e criaremos uma variável start no início da função main() para armazenar o tempo atual com := time.Now(). Descendo até o fim da função, após a execução da busca de preços, exibiremos o tempo total de execução com um fmt.Printf(), recebendo a mensagem "\nTempo total: %s\n" e a função time.Since(start):

package main

func main()
    start := time.Now()
    price1 := fetcher.FetchPriceFromSite1()
    price2 := fetcher.FetchPriceFromSite2()
    price3 := fetcher.FetchPriceFromSite3()
    
    fmt.Printf("R$ %.2f \n", price1)
    fmt.Printf("R$ %.2f \n", price2)
    fmt.Printf("R$ %.2f \n", price3)
    
    fmt.Printf("\nTempo total: %s", time.Since(start))
}

Executando novamente no terminal, veremos que o tempo total de execução foi de 6 segundos, somando os tempos de cada site.

R$ 0.87

R$ 44.75

R$ 72.06

Próximos passos

Na sequência, descobriremos como a concorrência pode nos ajudar a executar esse programa de forma mais eficiente.

Concorrência em Go - Goroutines

O objetivo agora é aplicar concorrência no projeto que estamos desenvolvendo. Primeiramente, precisamos entender como criar concorrência.

Entendendo o conceito de Goroutines

No Go, utilizamos a palavra-chave go para isso. Sempre que utilizarmos go seguido de uma função, estamos criando uma Goroutine (rotina do Go). Uma Goroutine é uma função ou método que será executado concorrentemente com outras Goroutines no mesmo espaço de endereçamento. Tudo isso é gerenciado pela runtime do Go.

Quando colocamos go antes de uma função, essa função será executada de forma concorrente com outras funções. Isso significa que queremos que a runtime do Go gerencie e execute essas funções de maneira sofisticada.

Adaptando o código para usar Goroutines

Nosso objetivo é executar as três chamadas de busca de preço dos sites 1, 2 e 3 em Goroutines separadas. Para isso, vamos voltar ao arquivo main.go.

No interior da função main(), recortaremos a linha price3 = fetcher.FetchPricesFromSite3() com o atalho "Ctrl + X" e o colocaremos entre as chaves de uma função anônima go func(), que criaremos abaixo da linha start := time.Now().

Queremos que essa função seja executada assim que chegar nessa instrução. Portanto, adicionaremos um par de parênteses após o fechamento das chaves.

Faremos o mesmo para as funções 2 e 3, utilizando uma função anônima para cada.

main.go:

// Código omitido

func main()
    start := time.Now()
    
    go func() {
        price1 := fetcher.FetchPricesFromSite1()
    }()

    go func() {
        price2 := fetcher.FetchPricesFromSite2()
    }()

    go func() {
        price3 := fetcher.FetchPricesFromSite3()
    }()

    fmt.Printf("R$ %.2f \n", price1)
    fmt.Printf("R$ %.2f \n", price2)
    fmt.Printf("R$ %.2f \n", price3)

    fmt.Printf("\nTempo total: %s", time.Since(start))
}

No entanto, enfrentamos um problema: os preços 1, 2 e 3 não estão visíveis. Precisamos saber de onde vêm esses preços, qual é o tipo desses dados e outras informações necessárias para manter o projeto funcionando.

Para resolver isso, criaremos variáveis acima das funções anônimas, chamadas price1, price2 e price3, todas do tipo float64. Em vez de inicializá-las imediatamente dentro das funções com o sinal :=, deixaremos apenas o sinal de igual, permitindo que o programa funcione corretamente.

// Código omitido

func main()
    start := time.Now()
    var price1, price2, price3 float64
    
    go func() {
        price1 = fetcher.FetchPricesFromSite1()
    }()

    go func() {
        price2 = fetcher.FetchPricesFromSite2()
    }()

    go func() {
        price3 = fetcher.FetchPricesFromSite3()
    }()

    fmt.Printf("R$ %.2f \n", price1)
    fmt.Printf("R$ %.2f \n", price2)
    fmt.Printf("R$ %.2f \n", price3)

    fmt.Printf("\nTempo total: %s", time.Since(start))
}

Após criar as variáveis e as três Goroutines, esperamos que elas sejam executadas de forma concorrente. Ao executar o projeto no terminal com go run cmd/main.go, perceberemos que ele exibe os preços zerados e o tempo de 46 microssegundos.

R$ 0,00

R$ 0,00

R$ 0,00

Tempo total: 46.167µs

Isso ocorreu porque as funções não tiveram tempo de concluir antes de exibir os preços.

Implementando o Wait Group

Para resolver isso, precisamos criar uma variável que nos permita esperar a conclusão das Goroutines. Abaixo da declaração das variáveis de preço, criaremos uma variável chamada wg, que significa Wait Group (grupo de espera).

O Go já possui isso nativamente no pacote sync. Para utilizá-lo, adicionaremos um sync.WaitGroup.

Após criar o Wait Group, precisamos informar que ele deve esperar a conclusão das três rotinas. Na linha seguinte, utilizaremos o método .Add() a partir do wg para adicionar um delta, que é um número inteiro representando a quantidade de Goroutines que ele deve gerenciar — neste caso, 3.

Em seguida, solicitaremos ao Wait Group que espere a conclusão de cada rotina. Para isso, abaixo das três chamadas de funções anônimas e antes de exibir os nomes, adicionaremos um wg.Wait() para solicitar essa espera.

// Código omitido

func main()
    start := time.Now()
    var price1, price2, price3 float64
    var wg sync.WaitGroup
    wg.Add(3)
    
    go func() {
        price1 = fetcher.FetchPricesFromSite1()
    }()

    go func() {
        price2 = fetcher.FetchPricesFromSite2()
    }()

    go func() {
        price3 = fetcher.FetchPricesFromSite3()
    }()
    
    wg.Wait()

    fmt.Printf("R$ %.2f \n", price1)
    fmt.Printf("R$ %.2f \n", price2)
    fmt.Printf("R$ %.2f \n", price3)

    fmt.Printf("\nTempo total: %s", time.Since(start))
}

Evitando o problema de deadlock

Após as alterações, ao executar o projeto novamente no terminal, ainda teremos os preços zerados, pois enfrentamos um problema de deadlock.

fatal error: all goroutines are asleep - deadlock!

Isso ocorre quando as Goroutines estão esperando umas pelas outras, mas a ação esperada não acontece, fazendo com que o programa fique preso. Precisamos informar ao Wait Group quando cada função é concluída.

Para isso, adicionaremos uma instrução de defer e utilizaremos um wg.Done() dentro de cada função, na primeira linha entre suas chaves. O defer garante que o Done() será executado após todas as linhas da função serem executadas, indicando que a função foi concluída.

// Código omitido

func main()
    start := time.Now()
    var price1, price2, price3 float64
    var wg sync.WaitGroup
    wg.Add(3)
    
    go func() {
        defer wg.Done()
        price1 = fetcher.FetchPricesFromSite1()
    }()

    go func() {
        defer wg.Done()
        price2 = fetcher.FetchPricesFromSite2()
    }()

    go func() {
        defer wg.Done()
        price3 = fetcher.FetchPricesFromSite3()
    }()
    
    wg.Wait()

    fmt.Printf("R$ %.2f \n", price1)
    fmt.Printf("R$ %.2f \n", price2)
    fmt.Printf("R$ %.2f \n", price3)

    fmt.Printf("\nTempo total: %s", time.Since(start))
}

Ao executar o projeto novamente com go run cmd/main.go, ele funcionará corretamente, levando apenas três segundos para buscar os preços em diferentes sites.

R$ 14,85

R$ 8,83

R$ 61,58

Tempo total: 3.001274333s

Próximos passos

Sabemos que a busca no primeiro site demora um segundo, a busca no segundo demora três segundos e a busca no terceiro demora dois segundos. Na sequência, entenderemos porque as três buscas foram realizadas tão rápido.

Sobre o curso Go: use concorrência para otimizar sua aplicação

O curso Go: use concorrência para otimizar sua aplicação possui 97 minutos de vídeos, em um total de 41 atividades. Gostou? Conheça nossos outros cursos de GoLang em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda GoLang acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas