Quando utilizar Goroutines?

Uma dúvida que assombra a maioria dos desenvolvedores Go, e não exclusivamente iniciantes, é sobre quando utilizar goroutines.

Nesse post vou dar algumas dicas para ajudar na análise e tomada de decisão na hora de adotar ou não a utilização de goroutines em seu projeto.

Antes de mais nada, assim como qualquer coisa relacionada a tecnologia, nem todo projeto faz sentido utilizar goroutines. Você pode até pensar, “meu sistema está lento. Já sei, vou usar goroutines para resolver.”, gastar muito tempo, já que SÓ colocar um go antes da chamada da função pode não ser o suficiente, e no final não ver melhora nenhuma ou até mesmo notar uma piora no desempenho.

Por isso, se você está pensando em adotar goroutines para melhorar o desempenho do seu sistema, eu sugiro que antes disso, faça um benchmark + profile do seu código para tentar identificar o que pode ser melhorado.

Se o seu caso não é esse, antes de adicionar goroutines, é bom analisar as funções que você utiliza em seu código e se elas não fazem uso de goroutines, como por exemplo o tratamento de requests HTTP.

A implementação do package net/http abre uma nova goroutine para cada request recebida. Sendo assim, não faz sentido que a cada request recebida você abra outra goroutine para lidar com a request.

Agora, se você já fez um benchmark + profile e entendeu o que as funções ao redor do seu código fazem, e você precisa/pode ter o processamento de tarefas de forma concorrente, aí sim chegou a hora de realmente pensar em goroutines.

Pensar em goroutines é mudar o pensamento comum e sequencial para um pensamento concorrente, ou seja, onde multiplas tarefas acontecerão ao mesmo tempo.

Como exemplo, vamos imaginar um endpoint que recebe o cadastro de uma pessoa. Esse cadastro, além das informações de texto, recebe 3 imagens (foto da pessoa, cpf e rg). Essas imagens são salvas em buckets na cloud onde a aplicação está hospedada.

func upload(img []byte) {
    // FAZ UPLOAD
}

func HandleAdd(w http.ResponseWriter, r *http.Request) {
    // PARSE BODY

    upload(p.Image)
    upload(p.Cpf)
    upload(p.Rg)

    // continua
}

Nesse cenário, poderiamos adicionar uma goroutine para o upload de cada imagem. Porém, como disse anteriormente, esse caso não seria um simples go func, pois é necessário que nossa goroutine principal saiba se houve algum erro ao tentar fazer o upload de qualquer uma das imagens.

Para conseguir tratar a resposta de cada um dos uploads, podemos ter um channel de tamanho 3, onde cada goroutine de upload vai escrever um erro caso não tenha conseguido fazer o upload da imagem.

É importante também que tenhamos criado um *sync.WaitGroup na goroutine principal para gerir a execução das goroutines de upload.

No final, para que o “é só adicionar um go na frente da chamada da função” funcionasse de forma correta, teríamos alterado nosso código para algo parecido com isso:

func upload(img []byte, cerr chan<- error, wg *sync.WaitGroup) {
    // FAZ UPLOAD
    cerr <- err
    wg.Done()
}

func HandleAdd(w http.ResponseWriter, r *http.Request) {
    // PARSE BODY
       
    var (
        errs := make(chan error, 3)
        wg sync.WaitGroup
    )
    wg.Add(3)

    go upload(p.Image, errs, wg)
    go upload(p.Cpf, errs, wg)
    go upload(p.Rg, errs, wg)

    wg.Wait()

    close(errs)

    for err := errs {
        if err != nil {
           // trata erro
        }
    }

    // continua
}

Goroutines são maravilhosas e podem trazer muitos beneficios para uma aplicação Go, porém é importante que sejam usadas com cuidado.

Meu conselho final para saber se convém adicionar uma goroutine à sua aplicação, é pensar se aquela tarefa ou conjunto de tarefas pode ser executada de forma concorrente e sem aumentar incrívelmente a complexidade para manutenção do código.

Ahh.. E nunca se esqueça de criar mecanismos para validação e controle das goroutines, para que elas não “saiam de controle” e acabem prejudicando sua aplicação.

Deixem suas dúvidas nos comentários.

Até a próxima!


Subscreva

Fique por dentro de tudo o que acontece no mundo Go.

2 comentários sobre “Quando utilizar Goroutines?

  1. Como iniciante fiquei com algumas dúvidas:
    1) Os WaitGroup são criados para que possamos ter um controle da quantidade de goroutines que estamos criando e para que possamos fechar as threads cridas por elas ?

    2) Quando usamos apenas a palavra “go” na frente de uma função que desejamos que seja executada de forma concorrente, não temos controle do que pode acontecer com ela, e nem temos como saber se ocorreu um erro ou se aquele processo foi frechado. É isso ?

    Parabéns pelo conteúdo, ficou muito bom!

    • Obrigado João, respondendo suas dúvidas.

      1) O WaitGroup funciona da seguinte forma ali. A cada Add, ele incrementa um contador interno em 1 e a cada Done, decrementa em 1. A função Wait faz com que a goroutine principal fique pausada até que o contador interno do WaitGroup seja 0. Ou seja, o WaitGroup não tem controle direto sobre as goroutines, ele serve só (no nosso exemplo) para que tenhamos um controle a parte de quantas estão abertas.

      2) A goroutine que “chamou” o “go”, só terá qualquer tipo de informação da outra goroutine caso tenha um channel de comunicação.

      Se não tiver sanado 100%, deixe outro comentário ai tento te ajudar.

Deixe uma resposta