Alura > Cursos de Programação > Cursos de GoLang > Conteúdos de GoLang > Primeiras aulas do curso Go e refatoração: melhorando códigos com boas práticas

Go e refatoração: melhorando códigos com boas práticas

Legibilidade e módulos - Apresentação

Olá! Meu nome é Guilherme Lima e estou muito feliz que você quer aprender mais sobre Go.

Audiodescrição: Guilherme é uma pessoa branca. Tem olhos castanhos e cabelos escuros curtos. Tem barba, usa óculos e está com uma camiseta polo azul. Ao fundo, parede com iluminação azulada e quadro abstrato.

O que vamos aprender?

Neste curso, vamos trabalhar em um projeto que tem um único arquivo main que realiza diversas funções, com quase 300 linhas de código. Este projeto utiliza Docker com um banco de dados no PostgreSQL e realiza várias operações. É uma API com duas entidades.

Nosso desafio será transformar um código extenso, feito em apenas um arquivo, em algo mais claro e organizado. Para isso, vamos criar uma pasta chamada "internal" com arquivos relacionados à configuração, handler, middleware, modelos, repositórios, routers, entre outros.

Faremos isso seguindo as principais diretrizes da engenharia de software, como boas práticas de programação, clean code (código limpo), arquitetura limpa, entre outros conceitos.

Se essa proposta despertou seu interesse, convidamos você a participar deste curso para aprender mais sobre essa incrível linguagem.

Legibilidade e módulos - Carregando o projeto base

Para começar, em um time de desenvolvimento no mundo real, seja com Go ou qualquer outra linguagem, é comum lidar com bases de código que não foram criadas por você. O desafio como pessoas desenvolvedoras e engenheiras de software, é tornar esse código legível para que outras pessoas possam dar manutenção, implementar novas funcionalidades e mantê-lo organizado.

Conhecendo o projeto

Já fizemos o download do projeto base que utilizaremos nesse curso. Esse projeto não segue boas práticas de programação. Nosso objetivo será melhorar a legibilidade e outros aspectos. Essa é uma habilidade essencial no desenvolvimento.

Apesar de ser mais fácil trabalhar em um código que nós mesmos criamos, onde conhecemos todos os detalhes, no mundo real, trabalhamos com códigos nos quais muitas pessoas já constribuíram. Algumas partes podem ser fáceis, enquanto outras são difíceis de entender. Vamos explorar isso na prática durante o curso.

O projeto que estamos analisando possui arquivos como .gitignore, docker-compose.yml, Dockerfile, entre outros. Trata-se de uma API desenvolvida em Go que utiliza o banco de dados PostgreSQL com Docker.

Nosso primeiro desafio é garantir que o Docker esteja instalado para executar o projeto. No nosso caso, o Docker já está instalado e em execução, como indicado pelo aviso "Docker Desktop is running" na barra de tarefas do computador.

Todos os detalhes sobre o código base, o Docker e sua instalação estão descritos na atividade chamada "Preparando o ambiente", anterior a este vídeo.

Subindo a aplicação

A primeira ação será subir o projeto usando o comando docker compose. Em versões mais antigas do Docker, é necessário utilizar docker-compose com hífen. Nas versões mais recentes, isso não é mais necessário. Caso o docker compose não esteja habilitado, basta atualizar o Docker ou usar o hífen entre essas palavras.

Nosso objetivo é subir a aplicação e construir a imagem, utilizando a opção --build.

docker compose up --build

Ao executar o comando, o Docker começa a carregar e preparar o container, subindo a API e o banco de dados. Enquanto isso, podemos verificar que o banco de dados está pronto para receber conexões, assim como a API.

Explorando docker-compose

No arquivo docker-compose.yml, há um serviço dedicado à API, que utiliza o PostgreSQL. Como estamos em um ambiente de desenvolvimento, não nos preocuparemos com variáveis de ambiente e outros aspectos importantes neste momento.

Analisando a imagem do banco de dados PostgreSQL, que está na versão 13, o Docker carrega e popula o banco de dados.

docker-compose.yml:

services:
  api:
    build: .
    ports:
      - "8080:8080"
    environment:
      - POSTGRES_USER=admin
      - POSTGRES_PASSWORD=admin
      - POSTGRES_DB=postgres
      - POSTGRES_HOST=db
      - POSTGRES_PORT=5432
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U admin -d itens -h db"]
      interval: 5s
      timeout: 10s
      retries: 30
      restart: always
  db:
    image: postgres:13
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d postgres -h db"]
      interval: 5s
      timeout: 10s
      retries: 30
      restart: always
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
volumes:
  postgres_data:

Através da linha volumes, o arquivo ./init.sql é carregado, criando e populando as tabelas no banco de dados. Por isso, iniciamos o projeto com algumas bases inseridas, simulando o mundo real.

init.sql:

-- Cria a tabela "itens"
CREATE TABLE IF NOT EXISTS itens (
    id SERIAL PRIMARY KEY,
    nome VARCHAR(100) NOT NULL,
    codigo VARCHAR(50) NOT NULL UNIQUE,
    descricao VARCHAR(255),
    preco DECIMAL(10,2) NOT NULL,
    quantidade INTEGER NOT NULL
);

-- Insere 50 itens com dados exemplares
INSERT INTO itens (nome, codigo, descricao, preco, quantidade) VALUES
('Teclado Mecânico', 'TEC001', 'Teclado mecânico com retroiluminação', 150.00, 20),
('Mouse Óptico', 'MOU002', 'Mouse óptico sem fio', 80.00, 50),
-- código omitido...

-- Cria a tabela "categorias"
CREATE TABLE IF NOT EXISTS cats (
    id SERIAL PRIMARY KEY,
    nome VARCHAR(100) NOT NULL,
    codigo VARCHAR(50) NOT NULL UNIQUE,
    descricao VARCHAR(300)
);

-- Insere 5 categorias de produtos
INSERT INTO cats (nome, codigo, descricao) VALUES
('Eletrônicos', 'ELEC', 'Produtos eletrônicos em geral.'),
-- código omitido...

Temos uma tabela de categorias e uma tabela de itens, mas não sabemos o tipo de relacionamento entre elas. Nosso objetivo inicial será entender como o código foi criado e dividido.

Analisando arquivo main.go

O Docker está funcionando corretamente, e a API já está recebendo solicitações pela porta 8080. Antes de mostrar a API em funcionamento, vamos acessar o arquivo main.go para analisar o código.

O arquivo main.go parece ser extenso. Vamos explorá-lo. Primeiro, declaramos que o código pertence ao package main.

Depois, importamos tudo o que é necessário. O projeto utiliza o pacote do json; fmt para imprimir algumas informações no terminal; log; requisições com o net/http; strconv para realizar a conversão de algumas informações.

Além disso, utiliza o Gorm, que é um ORM do Go, o que significa que não precisamos escrever código SQL o tempo todo. Assim, temos um caminho para acessar as informações do banco de dados.

main.go:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

Ainda no arquivo main.go, também encontramos códigos de modelos. Por exemplo, existe um type chamado Iten, que é uma struct relacionada a itens, com os campos Id, Nome, Codigo, Descricao, Preco e Quantidade. Também existe uma outra tabela de categorias, chamada Cat. Isso parece desorganizado, pois tudo está em um único arquivo.

// Modelo para a tabela "itens"
type Iten struct {
    Id          uint   `gorm:"primaryKey" json:"id"`
    Nome        string `gorm:"nome" json:"nome"`
    Codigo      string `gorm:"unique" json:"codigo"`
    Descricao   string `json:"descricao"`
    Preco       float64 `json:"preco"`
    Quantidade int    `json:"quantidade"`
}

// Modelo para a tabela "categorias"
type Cat struct {
    Id        uint   `gorm:"primaryKey" json:"id"`
    Nome      string `gorm:"nome" json:"nome"`
    Codigo    string `gorm:"unique" json:"codigo"`
    Descricao string `json:"descricao"`
}

Além disso, há uma variável global chamada bd, que provavelmente está relacionada ao banco de dados.

Na função main(), a primeira ação é realizar a conexão com o banco de dados. Se ocorrer um erro, ele indicará que não foi possível conectar ao banco de dados. Caso, não haja nenhum problema, fazemos a migração dos dados de item e categoria, utilizando o script SQL.

var bd *gorm.DB

func main() {
    // Conexão com o PostgreSQL (usando host "db" pois o docker-compose cria essa rede)
    dsn := "host=db user=postgres password=postgres dbname=postgres port=5432 sslmode=disable TimeZone=UTC"
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatalf("Erro ao conectar com o BD: %v", err)
    }
    bd = db

    // AutoMigrate para criar/ajustar tabelas
    bd.AutoMigrate(&Iten{})
    bd.AutoMigrate(&Cat{})

Em seguida, os endpoints começam a ser definidos, desde a linha 48 até a 63.

func main() {
    // código omitido…

    // Endpoint raiz
    http.HandleFunc("/api", indexHandler)

    // Endpoints para Itens
    http.HandleFunc("/itens", listItensHandler)                // GET para listar todos os itens
    http.HandleFunc("/itens/get", getItenHandler)              // GET para buscar um item (espera id via query: ?id=1)
    http.HandleFunc("/itens/get-code", getItenByCodigoHandler) // get-code?codigo=TEC001
    http.HandleFunc("/itens/create", createItenHandler)        // POST para criar um item
    http.HandleFunc("/itens/update", updateItenHandler)        // PUT para atualizar um item (JSON com id)
    http.HandleFunc("/itens/delete", deleteItenHandler)        // DELETE para deletar um item (espera id via query: ?id=1)

    // Endpoints para Categorias
    http.HandleFunc("/categorias", listCategoriasHandler)         // GET para listar todas as categorias
    http.HandleFunc("/categorias/get", getCategoriaHandler)       // GET para buscar uma categoria (espera id via query)
    http.HandleFunc("/categorias/create", createCategoriaHandler) // POST para criar uma categoria
    http.HandleFunc("/categorias/update", updateCategoriaHandler) // PUT para atualizar uma categoria (JSON com id)
    http.HandleFunc("/categorias/delete", deleteCategoriaHandler) // DELETE para deletar uma categoria (espera id via query)

    log.Println("Servidor rodando na porta 8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Vamos testar o primeiro endpoint, /api, pois deve mostrar algo. No navegador, digitamos o caminho localhost:8080/api e acessamos uma tela preta com o texto "API Go!". Isso indica que a API foi carregada com sucesso.

Dentre a série de endpoint disponíveis para itens, vamos acessar /itens. Assim, todos os itens são listados. Esses itens vêm do arquivo SQL que carregamos e incluem informações como nome, código, descrição e mais.

GET localhost:8080/api/itens:

[
    {
        "id": 1,
        "nome": "Teclado Mecânico",
        "codigo": "TEC001",
        "descricao": "Teclado mecânico com retroiluminação",
        "preco": 150,
        "quantidade": 20
    },
    {
        "id": 2,
        "nome": "Mouse Óptico",
        "codigo": "MOU002",
        "descricao": "Mouse óptico sem fio",
        "preco": 80,
        "quantidade": 50
    },
    ...
]

Também conseguir buscar um item específico via GET. Basta usar /itens/get seguido do sinal de interrogação e o ID do item que estamos buscando. Por exemplo, vamos buscar a lâmpada LED, cujo ID é 33.

GET localhost:8080/api/itens/get?id=33:

{
    "id": 33,
    "nome": "Lâmpada LED",
    "codigo": "LAM033",
    "descricao": "Lâmpada LED de alta eficiência",
    "preco": 50,
    "quantidade": 100
}

Dessa forma, visualizamos apenas o ID desejado.

Além disso, existem endpoints para criar, atualizar e deletar itens. É estranho que os nomes dos endpoints sejam exatamente aquilo que precisamos, como get, get-code, create. Parece uma prática antiga do XML. Não é assim que vamos trabalhar. Vamos melhorar esse código.

O mesmo ocorre para os endpoints de categorias. No navegador, vamos acessar localhost na porta 8080 e o endpoint de /categorias.

GET localhost:8080/categorias:

[
    {
        "id": 1,
        "nome": "Eletrônicos",
        "codigo": "ELEC",
        "descricao": "Produtos eletrônicos em geral."
    },
    {
        "id": 2,
        "nome": "Periféricos",
        "codigo": "PERI",
        "descricao": "Acessórios e periféricos para computadores."
    },
    ...
]

Assim, teremos as cinco categorias do init.sql: eletrônico, periféricos, informática, acessórios e eletrodomésticos.

O arquivo main é tão extenso, que ainda não acabamos de analisá-lo. Ainda temos as definições de cada função, incluindo o handler do index, o handler de listar itens e por aí vai.

Isso sem citar as funcionalidades que não poderemos testar no curso, por questões de tempo. Mas poderíamos testar a atualização e remoção de itens e categorias, além de validar IDs.

Conclusão

O grande problema desse código é a falta de organização. No livro "O Programador Pragmático", de Andrew Hunt e David Thomas, há uma recomendação:

"Organize seu código como se estivesse escrevendo um livro. Cada arquivo deve ter um propósito claro e um local óbvio."

Neste projeto, temos uma API funcional com CRUDs de itens e categorias, mas o código faz tudo em um único arquivo, sem separação por propósitos ou nomes claros.

Na próxima atividade, vamos começar a criar uma estrutura organizada, dividindo responsabilidades, seguindo a orientação do livro "O Programador Pragmático". É isso que exploraremos na sequência.

Legibilidade e módulos - Módulos

No livro "Clean Code" (Código Limpo), de Robert C. Martin, há uma frase interessante:

"Um código ruim pode funcionar, mas se ele cheirar mal, ele vai te custar caro no futuro."

Ou seja, escrever um código que funciona não é o maior desafio para quem trabalha com engenharia de software. Ele menciona que, se esse código que funciona "cheira mal" — o termo em inglês é Code Smell (cheiro de código) —, isso pode custar caro no futuro. A manutenção, a adição de novas funcionalidades ou até mesmo a correção de futuros bugs se tornará muito difícil quando temos um código que funciona, mas é ruim.

Qual é o grande desafio do código em nosso projeto? Não temos as responsabilidades divididas, tudo está em um único arquivo, e os nomes estão um pouco estranhos. Vamos aprender a refatorar esse código de forma prática.

Em primeiro lugar, sem grandes mudanças drásticas, queremos remover os modelos do arquivo main. Sempre que o código funcionar, devemos tentar reescrevê-lo da melhor forma possível. Será que vale a pena isolar esse trecho de código em outro local para facilitar a manutenção? Esse é um pensamento que precisamos manter daqui para frente.

Separando modelos

Vamos criar uma pasta chamada "internal" para manter todos os códigos referentes ao módulo em que estamos trabalhando. Dentro de "internal", criaremos outra pasta chamada "models", onde começaremos a criar os modelos.

A dúvida é: devemos criar um único arquivo chamado models.py onde ficarão todos os modelos? A recomendação é não fazer isso, porque o projeto vai crescer. Atualmente, estamos trabalhando com dois modelos, mas futuramente pode haver outras 50 pessoas trabalhando em 50 modelos diferentes. Portanto, criaremos um arquivo diferente para cada struct.

Em "models", vamos criar um arquivo chamado item.go e outro chamado categoria.go. Ambos nomes no singular.

No item.go, vamos indicar que esse arquivo faz parte do pacote de models. Em seguida, vamos remover o modelo de itens do código main, que é o type Iten struct. Assim, transferiremos toda essa responsabilidade para o arquivo item.go.

internal/models/item.go:

package models

type Iten struct {
    Id         uint    `gorm:"primaryKey" json:"id"`
    Nome       string  `json:"nome"`
    Codigo     string  `gorm:"unique" json:"codigo"`
    Descricao  string  `json:"descricao"`
    Preco      float64 `json:"preco"`
    Quantidade int     `json:"quantidade"`
}

Para a categoria, faremos o mesmo procedimento. Depois de indicar o pacote de models, movemos toda a struct relacionada ao modelo Cat para dentro do arquivo categoria.go.

internal/models/categoria.go:

package models

type Cat struct {
    Id        uint   `gorm:"primaryKey" json:"id"`
    Nome      string `json:"nome"`
    Codigo    string `gorm:"unique" json:"codigo"`
    Descricao string `json:"descricao"`
}

Resgatando referências

Com isso, o código principal quebra, pois as referências aos itens e as categorias não serão reconhecidas. Precisamos, de alguma forma, trazer essa informação do código "internal" para o nosso projeto.

A primeira ação será na linha 28 e na linha 29. Não queremos mais pegar o Iten que estava no código main.go, mas, sim, do arquivo models.Iten{}. Faremos a mesma alteração para a categoria, utilizando models.Cat{}.

main.go:

"myapi/internal/models"

// AutoMigrate para criar/ajustar tabelas
bd.AutoMigrate(&models.Iten{})
bd.AutoMigrate(&models.Cat{})

Vamos continuar verificando quais são as outras alterações necessárias. Na linha 68, na função listItensHandler() que lista os itens, devemos alterar a variável itens para uma lista de []models.Iten.

var itens []models.Iten

Na função getItenHandler(), vamos alterar a variável item para models.Iten. Também devemos fazer exatamente a mesma alteração nas variáveis das funções getItenByCodigoHandler(), createItenHandler() e updateItenHandler().

var item models.Iten

Mais adiante, na linha 148 na função deleteItenHandler(), também teremos uma lista de models.Iten{}.

if err := bd.Delete(&models.Iten{}, id).Error; err != nil {
    http.Error(w, "Erro ao deletar o item", http.StatusInternalServerError)
    return
}

Próximos passos

Desafio: Realize as alterações necessárias para resgatar as referências de categorias.

Todas as vezes que aparecer uma categoria, ela virá dos modelos como models.Cat. Queremos que todas as outras alterações sejam feitas dessa forma.

Na sequência, testaremos outros verbos da nossa aplicação para verificar se tudo está funcionando como anteriormente, mas com um código um pouco melhor.

Sobre o curso Go e refatoração: melhorando códigos com boas práticas

O curso Go e refatoração: melhorando códigos com boas práticas possui 100 minutos de vídeos, em um total de 53 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