Golang – Refatoração e Empacotando de um App

Minha trajetória como hobista sempre esteve entrelaçada com o desenvolvimento de scripts shell, ferramentas personalizadas que criei para atender às minhas necessidades cotidianas. Em cada desafio que meus trabalhos apresentavam, a solução muitas vezes emergia na forma de scripts bash. Durante esse período, envolvi-me em projetos na construção de shell script, como o mkbuild, um script destinado à geração de SlackBuilds, scripts usados na configuração, compilação e empacotamento de aplicativos para o Slackware Linux. Isto foi feito por volta de 2008 no grupo Slack.Sarava.org, que atualmente não se justifica mais, dado a pouca significância do Slackware no cenário Linux.

A incursão na linguagem Go não foi aleatória; ela marcou uma etapa de mudança em minha carreira profissional. A conversão de um script bash em um comando de terminal em Go foi um exercício prático, uma forma de aplicar e consolidar meus conhecimentos na linguagem. Embora minha experiência inicial com Go tenha sido através da condução de um mini curso de 12 horas para estagiários da empresa de um amigo, esse contato inicial plantou a semente para um engajamento mais profundo com a linguagem. Após um período focado no Flutter/Dart, estou retornando ao Go com o objetivo de revisitar e expandir meus conhecimentos.

Esse retorno se materializa na revisão e expansão dos artigos da série GoLang em meu blog. Estou aproveitando esta oportunidade para aprofundar em áreas que anteriormente não explorei em detalhes. Este artigo, em particular, é um reflexo desse processo de revisão. Nele, descrevo como transformei um código inicialmente concentrado em um único arquivo, típico de scripts shell, em um código Go mais estruturado e profissional. Esta abordagem, focada na organização e modularização do código, não foi apresentada nos artigos anteriores.

O Código do page2md

O Markdown tornou-se um padrão eficiente e simplista para a escrita de textos, especialmente quando integrado a plataformas como o WordPress. Por isso, tenho utilizado frequentemente tanto para a escrita quanto para o armazenamento local dos meus textos.

Recentemente, tenho realizado frequentemente o processo de baixar textos do meu blog e convertê-los novamente para Markdown. Como já mencionei, tarefas repetitivas tendem a se transformar em scripts shell em meu fluxo de trabalho. Desta vez, contudo, decidi utilizar essa necessidade como uma oportunidade para revisar e praticar meus conhecimentos em Go, transformando o processo em um código nesta linguagem:

package main
import (
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"unicode/utf8"
)
func trim(s string) string {
	return strings.TrimSpace(s)
}
func compileRegex() (*regexp.Regexp, *regexp.Regexp, *regexp.Regexp, *regexp.Regexp, *regexp.Regexp) {
	re1 := regexp.MustCompile(`.*:::.*\n`)
	re2 := regexp.MustCompile(`{#[^}]*}`)
	re3 := regexp.MustCompile(` {.wp-block-code}`)
	re4 := regexp.MustCompile("```\\s*\\{.*\\}")
	re5 := regexp.MustCompile(`^\-+$`)
	return re1, re2, re3, re4, re5
}
func applyMarkdownFilters(inputString string, re1, re2, re3, re4, re5 *regexp.Regexp) string {
	inputString = re1.ReplaceAllString(inputString, "")
	inputString = re2.ReplaceAllString(inputString, "")
	inputString = re3.ReplaceAllString(inputString, "")
	inputString = re4.ReplaceAllString(inputString, "```")
	lines := strings.Split(inputString, "\n")
	filteredLines := []string{}
	previousLine := ""
	for _, line := range lines {
		trimmedLine := trim(line)
		if len(previousLine) > 0 && re5.MatchString(trimmedLine) && utf8.RuneCountInString(trimmedLine) == utf8.RuneCountInString(previousLine) {
			filteredLines[len(filteredLines)-1] = "## " + previousLine
		} else {
			filteredLines = append(filteredLines, line)
		}
		previousLine = trimmedLine
	}
	return strings.Join(filteredLines, "\n")
}
func applyFilters(inputFile string, outputFile string) error {
	inputBytes, err := os.ReadFile(inputFile)
	if err != nil {
		return err
	}
	re1, re2, re3, re4, re5 := compileRegex()
	inputString := applyMarkdownFilters(string(inputBytes), re1, re2, re3, re4, re5)
	err = os.WriteFile(outputFile, []byte(inputString), 0644)
	if err != nil {
		return err
	}
	return os.Chmod(outputFile, 0644)
}
func downloadPage(url string) (string, error) {
	response, err := http.Get(url)
	if err != nil {
		return "", err
	}
	defer response.Body.Close()
	htmlBytes, err := io.ReadAll(response.Body)
	return string(htmlBytes), err
}
func htmlToMarkdown(html string) ([]byte, error) {
	markdown := strings.NewReader(html)
	cmd := exec.Command("pandoc", "-f", "html", "-t", "markdown")
	cmd.Stdin = markdown
	return cmd.Output()
}
func main() {
	if len(os.Args) < 2 {
		fmt.Println("Usage: page2md <url> [output_file.md]")
		os.Exit(1)
	}
	url := os.Args[1]
	outputFile := "page.md"
	if len(os.Args) > 2 {
		outputFile = os.Args[2]
	}
	html, err := downloadPage(url)
	if err != nil {
		fmt.Printf("An error occurred while downloading the page: %v\n", err)
		os.Exit(1)
	}
	cmdOutput, err := htmlToMarkdown(html)
	if err != nil {
		fmt.Printf("An error occurred while converting HTML to Markdown: %v\n", err)
		os.Exit(1)
	}
	tmpFile, err := os.CreateTemp("", "temp-*.md")
	if err != nil {
		fmt.Printf("An error occurred while creating a temporary file: %v\n", err)
		os.Exit(1)
	}
	defer os.Remove(tmpFile.Name())
	defer tmpFile.Close()
	_, err = tmpFile.Write(cmdOutput)
	if err != nil {
		fmt.Printf("An error occurred while writing to the temporary file: %v\n", err)
		os.Exit(1)
	}
	err = applyFilters(tmpFile.Name(), outputFile)
	if err != nil {
		fmt.Printf("An error occurred while applying filters: %v\n", err)
		os.Exit(1)
	}
	fmt.Printf("Page converted to %s\n", outputFile)
}

Embora o código apresentado seja relativamente simples e esteja desprovido de comentários, ele serve como um exercício prático na linguagem Go. Pretendo incluir comentários de documentação a seguir para elucidar seu funcionamento.

A execução do código se apoia no aplicativo de linha de comando Pandoc para realizar a conversão de HTML para Markdown. Inicialmente, considerei o uso do pacote html-to-markdown, mas após alguns testes, concluí que a implementação de filtros necessários para limpar o Markdown gerado seria mais trabalhosa do que o uso do Pandoc. O objetivo deste código é gerar um Markdown simplista, uma versão local dos meus artigos, e os resultados obtidos com o Pandoc se mostraram mais satisfatórios para este propósito. Admito que não explorei todas as possibilidades do pacote html-to-markdown para obter um resultado comparável ao do Pandoc.

Resumindo, este é o código básico, ainda sem documentação, logs de sistema ou uma estrutura de arquivos separada. Atualmente, o aplicativo requer pelo menos um argumento de entrada: a URL da página a ser baixada e convertida em Markdown. Um segundo argumento é opcional e determina o nome do arquivo de saída para o Markdown. Caso este argumento não seja fornecido, o Markdown será gravado em um arquivo padrão chamado “page.md“, conforme especificado na linha 91.

Usage: page2md <url> [output_file.md]

Refatoração e Organização em Pacotes

A refatoração do código envolveu, inicialmente, sua divisão em arquivos distintos, organizados de acordo com suas respectivas responsabilidades. Esse passo visa facilitar a manutenção e promover a escalabilidade do projeto. A estrutura proposta é a seguinte:

  1. main.go:
    • Arquivo contendo a função main(), que orquestra o fluxo do programa.
    • Responsável pela validação das entradas e pelo direcionamento às funções dos demais pacotes.
  2. converter/converter.go:
    • Aloca a função de conversão de HTML para Markdown, substituindo a funcionalidade anteriormente desempenhada pelo Pandoc, através da função htmlToMarkdown.
  3. downloader/downloader.go:
    • Hospeda a função responsável pelo download da página web, nomeada como downloadPage.
  4. markdown/markdown.go:
    • Contém todas as funções relacionadas à manipulação e ao processamento de Markdown, incluindoapplyMarkdownFilters, applyFilters e o compileRegex.
  5. utils/utils.go:
    • Este arquivo reúne funções auxiliares de utilidade geral, como trim, além de outras funções que serão adicionadas posteriormente.

A validação de entradas realizada em main.go será aprimorada em etapas subsequentes. A estrutura final da árvore de diretórios do projeto é ilustrada abaixo:

Bash

rudson@suzail:~$ tree page2md
page2md
├── main.go
├── converter
│   └── converter.go
├── downloader
│   └── downloader.go
├── markdown
│   └── markdown.go
└── utils
└── utils.go

Inicializando o go.mod do Projeto

O primeiro passo consiste na criação do go.mod do projeto. Isso pode ser feito executando o comando “go mod init <nome_do_módulo>” no diretório raiz do projeto:

Bash

rudson@suzail:~$ go mod init github.com/username/page2md

Substitua username pelo seu nome de usuário no GitHub. Caso o projeto seja para fins de teste ou desenvolvimento local, você pode optar por usar apenas o nome do projeto.

A execução deste comando resultará na criação de um arquivo go.mod com um conteúdo similar ao seguinte:

module github.com/username/page2md

go 1.21.6

O próximo passo é dividir o código conforme discutido anteriormente. Aconselho que faça a divisão das funções nos diferentes arquivos e depois retorne ao texto para considerar as mudanças necessárias.

Pacote Converter

Primeiramente, vamos organizar o pacote responsável pela conversão de HTML para Markdown. Para isso, a função htmlToMarkdown será transferida para o arquivo converter.go, com algumas modificações para se adequar ao sistema de pacotes do Go:

package converter
import (
	"os/exec"
	"strings"
)
// HtmlToMarkdown converts HTML content to Markdown format using the Pandoc tool.
func HtmlToMarkdown(html string) ([]byte, error) {
	markdown := strings.NewReader(html)
	cmd := exec.Command("pandoc", "-f", "html", "-t", "markdown")
	cmd.Stdin = markdown
	return cmd.Output()
}

A primeira alteração é a declaração do pacote converter na linha 1. O Go reconhecerá este arquivo como um pacote dentro da estrutura do page2md, algo que se tornará ainda mais evidente nos passos subsequentes. Aproveitei também para adicionar uma string de documentação à função e renomeá-la de htmlToMarkdown para HtmlToMarkdown. Como essa função deve ser acessível fora do pacote converter, seu nome deve começar com uma letra maiúscula, seguindo a convenção do Go para exportação de identificadores.

Pacote Downloader

Continuando com a organização do projeto, o próximo passo é estruturar o pacote downloader. Assim como no pacote anterior, começamos com a declaração do pacote, adicionamos a string de documentação à função e, para permitir que a função seja acessível fora do pacote, alteramos a primeira letra do nome da função para maiúscula.

package downloader
import (
	"io"
	"net/http"
)
// DownloadPage fetches the content of the given URL and returns it as a string.
func DownloadPage(url string) (string, error) {
	response, err := http.Get(url)
	if err != nil {
		return "", err
	}
	defer response.Body.Close()
	htmlBytes, err := io.ReadAll(response.Body)
	return string(htmlBytes), err
}

Neste código, a função DownloadPage é responsável por buscar o conteúdo de uma URL fornecida e retorná-lo como uma string. A função faz uso dos pacotes net/http para realizar a requisição HTTP e io para ler a resposta. A declaração “defer response.Body.Close()” garante que o corpo da resposta será fechado ao fim da execução da função, uma prática importante para evitar vazamentos de recursos.

Com essa estruturação, o código se torna mais modular e claro, facilitando a manutenção e a compreensão do funcionamento de cada parte do projeto.

Pacote Utils

O processo de organização e preparação é aplicado de forma similar ao pacote utils. Isso envolve declarar o pacote, adicionar comentários explicativos, strings de documentação e renomear funções que serão exportadas para permitir o acesso fora do pacote.

package utils
import (
	"net/url"
	"path"
	"strings"
)
// Trim removes all leading and trailing white spaces from a string.
func Trim(s string) string {
	return strings.TrimSpace(s)
}
// IsValidURL verifica se a string fornecida é uma URL válida.
func IsValidURL(toTest string) bool {
	_, err := url.ParseRequestURI(toTest)
	if err != nil {
		return false
	}
	u, err := url.Parse(toTest)
	if err != nil || u.Scheme == "" || u.Host == "" {
		return false
	}
	return true
}
// GetLastPartOfURL extrai a última parte da URL.
// Se a última parte terminar com '.html', essa extensão é removida.
func GetLastPartOfURL(rawURL string) (string, error) {
	parsedURL, err := url.Parse(rawURL)
	if err != nil {
		return "", err
	}
	// gate the last part from path
	lastPart := path.Base(parsedURL.Path)
	// remove '.html'
	lastPart = strings.TrimSuffix(lastPart, ".html")
	return lastPart, nil
}

Neste estágio, aproveitei para incorporar duas novas funções ao pacote utils. A função IsValidURL foi introduzida para avaliar a validade da URL fornecida ao comando. Já a função GetLastPartOfURL foi criada para extrair o nome final da URL, que pode ser utilizado como nome padrão para o arquivo Markdown de saída, especialmente útil quando o segundo argumento do aplicativo (nome do arquivo de saída) não é fornecido.

Pacote Markdown

Finalizamos a reestruturação do código com o pacote markdown, aplicando o mesmo processo utilizado nos pacotes anteriores.

package markdown
import (
	"os"
	"regexp"
	"strings"
	"unicode/utf8"
	"github.com/username/page2md/utils"
)
// compileRegex compiles all regular expressions used in applyMarkdownFilters.
// Returns pointers to the compiled regular expressions.
func compileRegex() (*regexp.Regexp, *regexp.Regexp, *regexp.Regexp, *regexp.Regexp, *regexp.Regexp) {
	re1 := regexp.MustCompile(`.*:::.*\n`)
	re2 := regexp.MustCompile(`{#[^}]*}`)
	re3 := regexp.MustCompile(` {.wp-block-code}`)
	re4 := regexp.MustCompile("```\\s*\\{.*\\}")
	re5 := regexp.MustCompile(`^\-+$`)
	return re1, re2, re3, re4, re5
}
// ApplyFilters reads the input file, applies markdown filters, and writes the result to the output file.
func ApplyFilters(inputFile string, outputFile string) error {
	inputBytes, err := os.ReadFile(inputFile)
	if err != nil {
		return err
	}
	re1, re2, re3, re4, re5 := compileRegex()
	inputString := ApplyMarkdownFilters(string(inputBytes), re1, re2, re3, re4, re5)
	err = os.WriteFile(outputFile, []byte(inputString), 0644)
	if err != nil {
		return err
	}
	// Explicitly set file permissions for readability by all users
	return os.Chmod(outputFile, 0644)
}
// ApplyMarkdownFilters applies several filters to clean up and format the markdown text.
// It uses regular expressions to remove or replace certain patterns in the input string.
func ApplyMarkdownFilters(inputString string, re1, re2, re3, re4, re5 *regexp.Regexp) string {
	// Apply regex-based replacements
	inputString = re1.ReplaceAllString(inputString, "")
	inputString = re2.ReplaceAllString(inputString, "")
	inputString = re3.ReplaceAllString(inputString, "")
	inputString = re4.ReplaceAllString(inputString, "```")
	// Process the lines of the markdown content
	lines := strings.Split(inputString, "\n")
	filteredLines := []string{}
	previousLine := ""
	for _, line := range lines {
		trimmedLine := utils.Trim(line)
		// Check if the current line and the previous line form a specific pattern
		if len(previousLine) > 0 && re5.MatchString(trimmedLine) && utf8.RuneCountInString(trimmedLine) == utf8.RuneCountInString(previousLine) {
			// Modify the previous line in the filtered output
			filteredLines[len(filteredLines)-1] = "## " + previousLine
		} else {
			// Add the current line to the filtered output
			filteredLines = append(filteredLines, line)
		}
		previousLine = trimmedLine
	}
	return strings.Join(filteredLines, "\n")
}

Note que a função compileRegex inicia com letra minúscula, indicando que sua visibilidade é limitada ao pacote markdown. Isso ocorre porque a função é utilizada apenas internamente no pacote e, portanto, não necessita ser exportada. Adicionei comentários descritivos ao código, uma prática recomendada que se torna particularmente útil à medida que a complexidade do código aumenta.

É importante destacar o import "github.com/username/page2md/utils" (linha 9), é adicionado automaticamente pelo goimports, gerenciado pelo VSCode, ao incluir na linha 57 a chamada utils.Trim(line). Isso demonstra a integração do pacote utils, utilizando a função Trim que foi renomeada e exportada no arquivo utils.go.

Ajustes no arquivo main.go

Por fim, vamos abordar os ajustes realizados no arquivo main.go, que serve como ponto de entrada do nosso aplicativo:

package main
import (
	"fmt"
	"log"
	"os"
	"github.com/username/page2md/converter"
	"github.com/username/page2md/downloader"
	"github.com/username/page2md/markdown"
	"github.com/username/page2md/utils"
)
func main() {
	log.SetPrefix("INFO: ")
	log.SetFlags(log.Ldate | log.Ltime)
	log.Println("Starting application")
	// Check for the correct number of arguments
	if len(os.Args) < 2 {
		log.Println("No URL provided, exiting")
		fmt.Println("Usage: page2md <url> [outputFileName.md]")
		os.Exit(1)
	}
	urlInput := os.Args[1]
	if !utils.IsValidURL(urlInput) {
		log.Fatalf("Invalid URL provided: %s\n", urlInput)
	}
	// Determine the output file name
	var outputFile string
	if len(os.Args) > 2 {
		outputFile = os.Args[2]
	} else {
		var err error
		outputFile, err = utils.GetLastPartOfURL(urlInput)
		outputFile += ".md"
		if err != nil {
			outputFile = "output.md"
		}
	}
	// Download the HTML content of the page
	log.Printf("Downloading HTML content from %s\n", urlInput)
	html, err := downloader.DownloadPage(urlInput)
	if err != nil {
		log.Fatalf("An error occurred while downloading the page: %v\n", err)
		os.Exit(1)
	}
	// Convert the HTML content to Markdown
	log.Println("Converting HTML content to Markdown format")
	cmdOutput, err := converter.HtmlToMarkdown(html)
	if err != nil {
		log.Fatalf("An error occurred while converting HTML to Markdown: %v\n", err)
		os.Exit(1)
	}
	// Create a temporary file to store the intermediate markdown content
	log.Printf("Creating temporary file for intermediate markdown content")
	tmpFile, err := os.CreateTemp("", "temp-*.md")
	if err != nil {
		log.Fatalf("An error occurred while creating a temporary file: %v\n", err)
		os.Exit(1)
	}
	defer os.Remove(tmpFile.Name())
	defer tmpFile.Close()
	_, err = tmpFile.Write(cmdOutput)
	if err != nil {
		log.Fatalf("An error occurred while writing to the temporary file: %v\n", err)
		os.Exit(1)
	}
	// Apply filters to the markdown content and write the result to the output file
	log.Printf("Applying filters and writing the result to %s\n", outputFile)
	err = markdown.ApplyFilters(tmpFile.Name(), outputFile)
	if err != nil {
		log.Fatalf("An error occurred while applying filters: %v\n", err)
		os.Exit(1)
	}
	log.Printf("Page converted to %s\n", outputFile)
}

Neste arquivo, introduzi um sistema de log para facilitar o rastreamento e a resolução de possíveis problemas durante a execução do aplicativo. As instruções log.SetPrefix("INFO: ") e “log.SetFlags(log.Ldate | log.Ltime)” configuram o prefixo e o formato do log, respectivamente.

A estrutura do código inclui:

  • Validação da URL fornecida.
  • Determinação do nome do arquivo de saída.
  • Download do conteúdo HTML da página.
  • Conversão do conteúdo HTML para o formato Markdown.
  • Criação de um arquivo temporário para armazenar o conteúdo intermediário em Markdown.
  • Aplicação de filtros ao conteúdo Markdown e escrita do resultado no arquivo de saída.

Os imports "github.com/username/..." foram adicionados para integrar as funções dos pacotes converter, downloader, markdown e utils ao arquivo principal. Isso ocorreu ao corrigir as chamadas às funções nos respectivos pacotes, demonstrando a modularidade e a organização do projeto.

Os logs adicionados em pontos-chave do processo fornecem feedback valioso sobre o andamento da execução, permitindo uma rápida identificação e correção de problemas.

Ajustes Final

Com as etapas de desenvolvimento concluídas, seu aplicativo está pronto para ser compilado e instalado no sistema.

Bash

rudson@suzail:~$ go install

O comando go install compilará e instalará o aplicativo na pasta $GOPATH/bin. Caso tenha configurado o ambiente Go corretamente, esta pasta já estará inclusa no PATH do sistema, permitindo que o aplicativo seja executado de qualquer local. Se desejar instalar o binário em um diretório específico, execute go build no diretório base do projeto. Isso irá compilar o aplicativo e gerar um executável no mesmo diretório. Você pode então mover o executável para o local desejado.

Para finalizar e enriquecer o projeto, sugiro duas adições importantes:

  1. README.md:
    • Crie um arquivo README.md no diretório raiz do projeto.
    • Inclua informações sobre o propósito do aplicativo, instruções de instalação, uso, e quaisquer dependências necessárias.
    • Adicionar exemplos de comandos e saídas esperadas pode facilitar a compreensão dos usuários sobre como o aplicativo funciona.
  2. Testes Unitários:
    • Desenvolva testes unitários para garantir que as funcionalidades individuais do aplicativo funcionam como esperado.
    • Utilize o pacote de testes do Go (testing) para criar os testes.
    • Testes bem escritos ajudam na manutenção do código e garantem que futuras alterações não quebrem funcionalidades existentes.

Esses ajustes finais não apenas garantem que seu aplicativo esteja funcionando corretamente, mas também facilitam o uso por outros desenvolvedores, contribuindo para um projeto mais robusto e comunitário.

Considerações Finais

Ao longo deste artigo, exploramos o processo de estruturação e modularização do código de um aplicativo Go. O processo culminou na evolução deste código para um projeto Go bem organizado, dividido em pacotes distintos e responsáveis por tarefas específicas.

Passamos pela criação de pacotes como converter, downloader, markdown, e utils, cada um encapsulando funcionalidades lógicas e promovendo a reutilização de código. A importância de boas práticas de programação, como a adição de comentários de documentação e a conformidade com as convenções de nomenclatura em Go, foi enfatizada para manter o código acessível e manutenível.

O arquivo main.go serve como núcleo do aplicativo, integrando todos os componentes e gerenciando o fluxo de execução. A inclusão de um sistema de log oferece uma camada adicional de transparência, permitindo o monitoramento e a resolução eficiente de problemas.

Os ajustes finais abordaram a compilação e a instalação do aplicativo, juntamente com recomendações para a adição de um arquivo README.md e a implementação de testes unitários. Estas etapas finais são cruciais para assegurar a qualidade, a usabilidade e a durabilidade do software.

Este projeto demonstra não apenas a capacidade do Go em facilitar a criação de aplicativos eficientes e confiáveis, mas também ressalta a importância de uma arquitetura bem pensada. A modularização e a clareza do código não apenas facilitam a manutenção, mas também tornam o processo de desenvolvimento mais agradável e produtivo.

Espero que este artigo sirva como um guia útil para seus próprios projetos em Go, inspirando-o a adotar práticas que realcem a qualidade e a sustentabilidade do seu código.

Deixe um comentário

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.