Categorias
Desenvolvimento de Software Golang

Me aventurando na vera com GoLang e 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!

Categorias
Engenharia de Software Metodologias Ágeis

Metodologias Ágeis? O que tem isso de bom?

As Metodologias Ágeis de desenvolvimento de software estão em evidência nos últimos tempos. O interessante é observar o porquê dessa evidência.

Essas metodologias apresentam práticas que não são novidades, como a utilização de um ciclo de vida iterativo e incremental, por exemplo, que é também, encontrada no ciclo de vida do framework RUP (Rational Unified Process) e em outras metodologias mais “tradicionalistas”. Por muito tempo as empresas, por puro modismo, passaram a utilizar processos baseados em RUP e obtendo consequências alarmantes quando, por falta de bom senso e não por conta da metodologia (Vale ressaltar!), emperram o seu processo na burocracia e na preocupação em cumprir a risca o próprio processo.

Um processo de desenvolvimento de software é um conjunto de atividades distribuídas em várias fases de um ciclo de vida, que possuem o objetivo geral de se produzir um software de qualidade. Por muito tempo as empresas que desenvolvem software, sejam empresas que tenham o software como fim ou como meio, viveram no meio de um caos que pode até mesmo ser o que eu chamo de “caos em falso controle”. Nesse “caos em falso controle” as empresas possuem um processo bem denifido e até bem conhecido por seus funcionários, mas ainda sim não conseguem obter qualidade nos seus produtos, sofrendo ainda com estouros de prazos como regra e não exceção, o que também tem como consequência estouro nos custos planejados como regra, custos que acabam por serem repassados muitas vezes para o cliente.

Em 2001 ocorreu um movimento em que envolveu 17 pessoas, dentre essas engenheiros de software, consultores e desenvolvedores que criticavam algumas práticas de Engenharia de Software. Então essas pessoas assinaram um manifesto que propunha alguns valores para o desenvolvimento ágil de software, são os valores:

  • Indivíduos e interação entre eles mais que processos e ferramentas
  • Software em funcionamento mais que documentação abrangente
  • Colaboração com o cliente mais que negociação de contratos
  • Responder a mudanças mais que seguir um plano
Movimento Ágil
Movimento Ágil

Esses valores apresentados no manifesto ágil são os prováveis motivos da evidência das metodologias ágeis,visto que entre as pessoas que assinaram o manifesto encontram-se os criadores de várias metodologias como XP, SCRUM, entre outras. Eles declararam que mais importante que seguir um processo massivo apenas por que a empresa o adotou é saber o que as pessoas pensam disso, pensar em motivar as pessoas que desenvolvem os projetos de software, alimentar a comunicação, a franqueza e a colaboração dessas pessoas entre si de modo que todos entendam que possuem um objetivo comum, desenvolver um produto de qualidade. Mais importante do que fazer documentação em excesso é desenvolver um software que funcione corretamente e atenda as espectativas do cliente. É importante colaborar com o cliente, mais do que negociar novos contratos. Eles entendem que mudança é algo bom, desde que essa mudança agregue valor ao produto para o cliente. Relizar mudanças envolve obter um software de qualidade, vendo qualidade do ponto de vista da satisfação do cliente.

Como disse antes, algumas práticas das metodologias ágeis não são novidades, mas o novo foco apresentado pelos valores e princípios do movimento ágil agradam e estão dando muitos resultados, porém se as empresas derem importância apenas as práticas apresentadas pelas diversas metodologias, esquecendo dos valores e princípos do Agilemanifesto.org, essas também tenderam a ser mais algumas metodologias que não funcionam e causam novos problemas, e serão apresentadas como péssimos exemplos por futuros “xiitas” que terão novas metodologias como a “bala de prata” que resolve todos os problemas da engenharia de software.