Categorias
Desenvolvimento de Software Golang

Me aventurando na vera com GoLang e Goroutines

Um breve texto sobre minhas primeiras impressões com Golang e sobre a resolução de um problema com goroutines.

Há alguns meses eu tive o primeiro contato com a linguagem Go em um trabalho freelancer para uma startup que trabalha com movimentações financeiras que possibilitam a clientes empresariais o gerenciamento de gastos com alimentações, hospedagens, frotas de veículos, entre outras diversas coisas. As soluções de servidor desenvolvidas nessa startup são, em sua maioria, escritas nessa linguagem.

Nesse post, vou comentar um pouco sobre as minhas impressões sobre a linguagem nesse primeiro contato e vou falar sobre uma dificuldade que passei com as Goroutines e a solução que a linguagem oferece para o problema que enfrentei.

Primeiro contato com Go

Meu primeiro contato com a linguagem foi uma mistura de sensações. Primeiro veio a imensa animação ao perceber que é uma linguagem compilada dita moderna, pois é uma linguagem criada para um mundo dos modernos processadores multicores e focada na concorrência. Daí começo a estudar e percebo uma linguagem C-like, ou seja, uma linguagem bem semelhante à C na sintaxe e na tratativa de conceitos já transparentes em outras linguagens mais modernas como ponteiros, por exemplo. Minha primeira reação ao descobrir os ponteiros foi: “Por que trazer esse nível de dificuldade pro programador?”. Um tempo depois passei a pensar: “Por que não dar ao programador a possibilidade de otimizar o uso de memória através de ponteiros?”.

tenor[1]

Passado o conflito de sensações com a sintaxe base da linguagem e o fato de lidar com ponteiros, eu “descobri” como a linguagem é essencialmente preparada para ser concorrente e trás um conceito interessante chamado goroutine. A goroutine é uma forma de invocar uma determinada função em uma thread concorrente, inclusive compartilhando a memória. Segue um exemplo simples de uma goroutine:

package main

import (
	"fmt"
	"time"
)

func main() {
	caminho := "Fora da goroutine."
	
	// Função anônima a ser executada em outra thread
	go func(){
		caminho = "Dentro da goroutine."
		fmt.Println("Imprime depois!")
		fmt.Println(caminho)
		caminho = "Valor final."
        }()
	fmt.Println("Imprime primeiro!")
	fmt.Println(caminho)
	time.Sleep(1000 * time.Millisecond)
	fmt.Println(caminho)
}

Abaixo segue a saída para o código acima que demonstra a execução em thread paralela e o compartilhamento de variável entre as threads.

Imprime primeiro!
Fora da goroutine.
Imprime depois!
Dentro da goroutine.
Valor final.

Isso veio muito bem a calhar com o problema que eu tinha para resolver.

O problema

Temos uma situação em que precisamos permitir que o usuário do nosso sistema faça o upload de um arquivo que deverá ser processado e atualizar uma série de registros no sistema. Porém, esse arquivo pode ser grande e não gostaríamos de deixar o usuário esperando pela conclusão desse processamento.

A solução disparar o processamento do arquivo de modo assíncrono, isto é, o usuário recebe a confirmação de que o arquivo foi carregado, mas o processamento está sendo realizado em paralelo. Nesse caso, ele poderá consultar o resultado do processamento do arquivo posteriormente ou ser avisado.

Para essa solução assíncrona, poderíamos implementar de algumas formas, por exemplo:

  • Agendar o processamento em uma rotina periódica, executada de tempos em tempos;
  • Utilizar mecanismo de mensageria para disparar uma mensagem solicitando o processamento do arquivo pelo o recebedor da mensagem em momento posterior.

Porém, ambas as formas apresentadas acima exigiriam implementar um solução de gerenciamento de rotinas temporais ou implementação/integração de mecanismo de mensageria, essa última solução será muito interessante se passarmos a necessitar que esse tipo de processamento seja feito de modo distribuído.

A goroutine veio a calhar, pois basta uma thread concorrente para resolver o problema. E assim foi feito. Veja um exemplo de código abaixo.

func carregaArquivo(arquivo) string {
   // Abrindo transação
   // Código do upload do arquivo
   // Salvando no banco o estado de recebimento do arquivo

   // Função anônima a ser executada em outra thread
   go func(){
      // Processando arquivo em thread concorrente
      processandoArquivo(arquivo)
   }()
   // Confirmando transação
   return "OK"
}

Como você pode observar nos comentários do código acima, uma transação foi aberta para confirmar as opções. E é aí que eu comecei a ver que a solução aparentemente simples aliada a pouca experiência com a linguagem Go, tem consequências.

Consequências do uso da goroutine

1ª Consequência: A transação não é thread safe

No código acima, a goroutine é acionada dentro de uma transação de banco de dados. Porém a transação não é thread safe. Isso significa que quando a função carregaArquivo(arquivo) é executada a transação é encerrada, então quando a goroutine é executada em outra thread, ao finalizar a transação não existe mais, o que gera um panic  que vem a parar a aplicação, visto quê não há tratamento de recuperação no código.

Por tanto, não envolva uma a goroutine na transação da thread original. O código abaixo mostra como resolvi abrindo uma transação para cada thread.

func carregaArquivo(arquivo) string {
   // Abrindo transação
   // Código do upload do arquivo
   // Salvando no banco o estado de recebimento do arquivo
   // Confirmando transação

   // Função anônima a ser executada em outra thread
   go func(){
      // Abrindo transação
      // Processando arquivo em thread concorrente
      processandoArquivo(arquivo)
      // Confirmando transação
   }()
   return "OK"
}

2ª Consequência: Erros na goroutine podem encerrar a aplicação

Um código errado ou um erro de validação do arquivo não tratado me revelou que um erro na goroutine que gera um panic encerra minha aplicação. Como já mencionei na consequência anterior, isso ocorre por não ter me preocupado com a recuperação de erros.

Para fazer o controle de erros e evitar que o programa encerre, eu usei dois mecanismos de controle de fluxo bem interessantes da linguagem Go:

  • defer – Possibilitar adiar a execução de uma função para que ela seja chamada imediatamente após o retorno da função circundante.
  • recover – É uma função interna que possibilita recuperar o controle de uma função em pânico.

Veja mais sobre defer, panic e recover no artigo do blog da Golang aqui.

Segue abaixo o código que me permitiu fazer a recuperação de erro e evitar que a aplicação encerrasse por falha na goroutine.

func carregaArquivo(arquivo) string {
   // Abrindo transação
   // Código do upload do arquivo
   // Salvando no banco o estado de recebimento do arquivo
   // Confirmando transação

   // Função anônima a ser executada em outra thread
   go func(){ // Goroutine 1
      // O defer adia a execução dessa função para após
      // o retorno da Goroutine 1
      defer func() {
        if err := recover(); err != nil {
            // Entra aqui se tiver valor "err", pois indica um erro recuperado
            // Código que marca no banco o arquivo como
            // situação "Falha no Processamento" e loga o erro.
        }
      }()  // Abrindo transação // Processando arquivo em thread concorrente processandoArquivo(arquivo)  // Confirmando transação }() return "OK" }

Concluindo

Bom, provocar alguns erros nos testes aliado as experiências já vividas em outras linguagens, me fizeram perceber que o simples é bom, mas também tem consequências. Explorar essas consequências te ajuda a melhorar teu código e as tuas soluções. Por fim, depois de alguns poucos meses trabalhando com essa linguagem, estou me adaptando bem com Go agora. Penso em explorar outros aspectos dela aqui no futuro, por exemplo comparando microserviços semelhantes com Go e Java (Spring Boot), descrever a estrutura de um projeto com microserviços, quem sabe o quê mais?!

Fique a vontade para criticar, sugerir e entrar em contato para trocar uma ideia. Me interesso em aprender mais com discussões, confesso que gosto mais de discutir do que de escrever posts.

Abraço e até o próximo post!

Por Emerson Henrique

Perfil no http://www.linkedin.com/in/emersonhss

Deixe um comentário