Criando servidores robustos com GoLang

Criando servidores robustos com GoLang

Atenção: esta lição está intimamente ligada com a lição anterior que fala sobre servidores dinâmicos e estáticos com GoLang. Portanto se você ainda não viu, recomendo que volte um passo anterior.

Hoje, nós iremos aprender a construir aplicações web organizadas e robustas usando a linguagem GoLang, e para que isso seja possível, precisamos explorar alguns padrões arquiteturais mais comuns aplicados em projetos feitos em Go.

Além disso, precisamos discutir também quais são as abordagens frequentemente usadas para estruturar aplicações robustas (tanto front-end quanto back-end) em GoLang.

Então, sente que lá vem história 😊

O que são padrões arquiteturais, e como eles podem ajudar nossas aplicações em GoLang?

Padrões arquiteturais são “receitas” ou diretrizes para organizar a estrutura geral de um sistema de software—definindo como camadas, componentes e responsabilidades se relacionam.

Um padrão desse tipo, nada mais é do que uma forma organizada de separar suas roupas e objetos dentro do seu guarda-roupas. Ou será que você é aquele tipo de pessoa que costumam misturar tudo (desorganizado)?

Sendo assim, em vez de se concentrar em detalhes de código ou algoritmos (que seriam “padrões de projeto”), um padrão arquitetural descreve como dividir sua aplicação em partes (camadas, módulos, serviços), quais dependências cada uma pode ter e como elas se comunicam.

A adoção de padrões arquiteturais traz benefícios diretos às aplicações em Go (ou em qualquer outra linguagem), tais como:

  • Separação de Responsabilidades
  • Testabilidade
  • Manutenibilidade e Evolução
  • Escalabilidade e Colaboração
  • Consistência entre Projetos

Atualmente existe alguns padrões arquiteturais que costumam ser utilizados em GoLang e também adotados em outras linguagens de programação.

Veremos o funcionamento de cada um deles a seguir 😋

Padrão MVC (Model-View-Controller)

O padrão MVC (Model-View-Controller) é um dos padrões mais conhecidos e adotadas em aplicações web e desktop. Ele propõe a separação da aplicação em três componentes principais, cada um com responsabilidades bem definidas.

Começando pelo Model, ele é camada de dados e lógica de negócio. Representa entidades do domínio (por exemplo, “Usuário”, “Produto”, “Pedido”) e encapsula suas regras de validação, cálculos e persistência.

Vejamos um exemplo de um arquivo em GoLang chamado user.go que representa um Model:

// models/user.go
package models

import (
    "errors"
    "regexp"
)

// User representa a entidade "usuário" no sistema
type User struct {
    ID    int
    Name  string
    Email string
    Age   int
}

// IsValid verifica regras de negócio básicas antes de salvar/atualizar
func (u *User) IsValid() error {
    if u.Name == "" {
        return errors.New("nome é obrigatório")
    }
    // Valida formato de e-mail (regex simplificada)
    matched, _ := regexp.MatchString(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`, u.Email)
    if !matched {
        return errors.New("e-mail inválido")
    }
    if u.Age < 0 {
        return errors.New("idade não pode ser negativa")
    }
    return nil
}

o User (Model) é consumido pelos controllers (ou services) para criar/atualizar usuários, sendo responsável apenas pela lógica e pelos dados, sem preocupação com rotas ou apresentação.

Já a View, é camada de apresentação. Em aplicações web, geralmente consiste em templates HTML (com placeholders ou diretrizes para injeção de dados). Em desktop, pode ser formulários, janelas e componentes gráficos. A View só se preocupa em exibir informações ao usuário, sem lógica de negócio.

Vejamos um exemplo de um arquivo em GoLang chamado user_list.html que representa uma View:

<!-- templates/user_list.html -->
<!DOCTYPE html>
<html lang="pt-br">
<head>
    <meta charset="UTF-8">
    <title>Lista de Usuários</title>
</head>
<body>
    <h1>Usuários Cadastrados</h1>
    <ul>
        {{range .Users}}
            <li>{{ .Name }} – {{ .Email }}</li>
        {{else}}
            <li>Nenhum usuário encontrado.</li>
        {{end}}
    </ul>
    <a href="/users/new">Cadastrar novo usuário</a>
</body>
</html>

Nesse exemplo, o template espera um contexto (por exemplo, uma struct contendo Users []User) para preencher dinamicamente a lista. A View não faz consultas nem lógicas complexas, apenas exibe o que recebe.

Por fim, temos o nosso Controller (controlado), que é a camada intermediária que recebe requisições, interage com o Model para executar operações (CRUD, chamadas de serviço, validações) e, por fim, escolhe qual View (template) deve ser renderizada ou qual resposta (JSON, redirecionamento) deve ser retornada.

Vejamos um exemplo de um arquivo em GoLang chamado de user_controller.go:

// controllers/user_controller.go
package controllers

import (
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
    "myapp/models"
    "myapp/repositories"
)

// UserController agrupa handlers de usuário
type UserController struct {
    Repo repositories.UserRepository
}

// List lista todos os usuários
func (uc *UserController) List(c *gin.Context) {
    users, err := uc.Repo.GetAll()
    if err != nil {
        c.HTML(http.StatusInternalServerError, "error.html", gin.H{"error": err.Error()})
        return
    }
    // Renderiza o template "user_list.html", passando a lista de usuários
    c.HTML(http.StatusOK, "user_list.html", gin.H{"Users": users})
}

// Create exibe formulário de criação de usuário ou processa o POST
func (uc *UserController) Create(c *gin.Context) {
    if c.Request.Method == "GET" {
        // Exibe o formulário
        c.HTML(http.StatusOK, "user_form.html", nil)
        return
    }
    // Se for POST, tenta criar
    name := c.PostForm("name")
    email := c.PostForm("email")
    ageStr := c.PostForm("age")
    age, _ := strconv.Atoi(ageStr)

    user := &models.User{Name: name, Email: email, Age: age}
    if err := user.IsValid(); err != nil {
        c.HTML(http.StatusBadRequest, "user_form.html", gin.H{"Error": err.Error()})
        return
    }

    if err := uc.Repo.Save(user); err != nil {
        c.HTML(http.StatusInternalServerError, "error.html", gin.H{"error": err.Error()})
        return
    }

    c.Redirect(http.StatusSeeOther, "/users")
}

No caso do código acima, ele está executando as seguintes operações:

  • O Controller associa a rota /users ao método List e /users/new a Create.
  • Para GET /users, chama List, que busca a lista de usuários via uc.Repo.GetAll() (Model/Repositorio) e renderiza a View user_list.html.
  • Para GET /users/new, Create mostra o formulário (user_form.html).
  • Para POST /users/new, colhe os dados do formulário, valida via user.IsValid() (Model), salva via uc.Repo.Save(user) e, em caso de sucesso, redireciona para a lista.

Só que para isso tudo ser possível, você vai precisar também organizar cada um dos arquivos em suas respectivas pastas do projeto. Se tratando do padrão MVC, nós costumamos ter 5 pastas principais, por exemplo:

meu-projeto/
├── controllers/
├── models/
├── repositories/
├── views/ # ou templates
├── public/                  # arquivos estáticos (CSS, JS, imagens)
│   ├── css/
│   ├── js/
│   └── images/
├── main.go                  # inicialização de rotas, configuração do servidor
└── go.mod

controllers/: contém os handlers que recebem http.ResponseWriter e *http.Request (ou abstrações de frameworks) e chamam o Model/Repository.

models/: contém structs que representam as entidades e métodos de validação.

repositories/: responsáveis por interagir com o banco (SQL, ORM, chamada a APIs externas). Fornecem funções como GetAll, FindByID, Save, Update, Delete.

views/: arquivos HTML que utilizam {{ }} para interpolar valores e estruturas de controle (range, if).

public/: arquivos estáticos servidos diretamente (CSS, JavaScript, imagens).

E por fim o nosso arquivo main.go, que contém toda a lógica responsável por carregar os templates, ajustar as rotas e carregar o servidor como um todo, vejamos um exemplo de um arquivo main.go:

package main

import (
    "log"
    "net/http"
    "myapp/controllers"
    "github.com/gorilla/mux"
    "html/template"
    "path/filepath"
)

func main() {
    // Carrega templates globais
    templates := template.Must(template.ParseGlob(filepath.Join("templates", "*.html")))

    // Inicializa repositórios (conexão ao banco seria feita aqui)
    userRepo := repositories.NewUserRepository(/*db*/)
    productRepo := repositories.NewProductRepository(/*db*/)

    // Inicializa controllers
    userCtrl := &controllers.UserController{Repo: userRepo, Templates: templates}
    productCtrl := &controllers.ProductController{Repo: productRepo, Templates: templates}

    // Configura roteador com Gorilla Mux
    r := mux.NewRouter()
    // Rotas de usuário
    r.HandleFunc("/users", userCtrl.List).Methods("GET")
    r.HandleFunc("/users/new", userCtrl.CreateGet).Methods("GET")
    r.HandleFunc("/users/new", userCtrl.CreatePost).Methods("POST")
    // Rotas de produto
    r.HandleFunc("/products", productCtrl.List).Methods("GET")
    r.HandleFunc("/products/new", productCtrl.CreateGet).Methods("GET")
    r.HandleFunc("/products/new", productCtrl.CreatePost).Methods("POST")

    // Servir arquivos estáticos
    fs := http.FileServer(http.Dir("public"))
    r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fs))

    log.Println("Servidor rodando em http://localhost:8080")
    if err := http.ListenAndServe(":8080", r); err != nil {
        log.Fatal(err)
    }
}

Colocando em prática o padrão MVC

Certo, agora que já possuímos uma ideia de como criar nossos projetos web seguindo o padrão MVC, vamos ver na prática como ele se comporta.

Mas antes, precisamos definir o que queremos construir, certo? 😎

A ideia é que façamos uma pequena aplicação que conte com duas rotas, a primeira que é a principal (/) será responsável por mostrar uma lista de usuários cadastrados no sistema. 

A segunda (/add) será responsável por conter um formulário que vai adicionar as informações dos novos usuários.

E é claro, precisamos de uma rota do tipo POST para tratar o recebimento de dados da rota anterior.

A ideia é que façamos a separação dos arquivos de rotas em um arquivo chamado routes.go, e que tenhamos dois templates em HTML: home.html e add.html.

Vamos colocar a mão na massa? Primeiro comece criando a pasta do seu projeto caso não houver, no meu caso, dentro da pasta JornadaGoLang eu criei uma nova pasta chamada 24-criando-servidores-robustos-em-go.

Onde dentro dela, criei uma subpasta chamada de padrão-mvc, onde dentro dela (novamente rs) iniciei um novo projeto em Go:

go mod init padrao-mvc

Perfeito, agora nós temos a base necessária para começar construir o nosso projeto seguindo o padrão MVC 🤩

Passo 1) Crie as pastas necessárias, que organizarão o nosso projeto:

No diretório padrão-mvc (ou no nome que você escolheu para a pasta do projeto), organize da seguinte forma:

padrão-mvc/
├── controllers/
├── models/
├── routes.go
├── main.go
└── templates/
    ├── home.html
    └── add.html

controllers/: aqui ficarão os arquivos responsáveis por “tratar” as requisições (controladores).

models/: conterá as structs e funções de acesso aos dados (model).

routes.go: arquivo para registrar todas as rotas da aplicação.

main.go: ponto de entrada da aplicação, onde inicializamos o servidor.

templates/: pasta para armazenar os dois templates HTML que usaremos (home.html e add.html)

Passo 2) Crie o Model (models/user.go)

Em models/user.go, definimos a entidade User e as funções para gerenciar a lista de usuários em memória:

// models/user.go
package models

// User representa um usuário cadastrado no sistema
type User struct {
	Name  string
	Email string
}

// slice que armazenará todos os usuários (em memória)
var users []User

// init inicializa a lista de usuários (vazia) ao carregar o pacote
func init() {
	users = []User{}
}

// GetAll retorna todos os usuários cadastrados
func GetAll() []User {
	return users
}

// Add adiciona um novo usuário à lista
func Add(u User) {
	users = append(users, u)
}

Ali em cima, definimos uma struct User com dois campos: Name e Email, e junto a isso, criamos um slice users []User que ficará na memória durante toda a execução.

Além é claro, das funções:

  • GetAll(), que devolve o slice completo (para listagem).
  • Add(u User) simplesmente faz append no slice global, simulando o “salvar no banco”.

Passo 3) Crie o Controller (controllers/user_controller.go)

Em controllers/user_controller.go, vamos implementar a lógica para listar e adicionar usuários. Observe que usamos HTML templates carregados de fora, mas a ideia principal é separar lógica de “tratamento de requisição” (controller) de “dados” (model):

// controllers/user_controller.go
package controllers

import (
	"html/template"
	"net/http"
	"padrao-mvc/models"
)

// UserController agrupa os métodos que manipulam usuários
type UserController struct {
	Templates *template.Template
}

// NewUserController recebe a instância de templates (pré-carregados) e retorna o controller
func NewUserController(tmpl *template.Template) *UserController {
	return &UserController{Templates: tmpl}
}

// ListUsers exibe a página principal ("/") com a lista de usuários cadastrados
func (uc *UserController) ListUsers(w http.ResponseWriter, r *http.Request) {
	// Recupera todos os usuários do Model
	allUsers := models.GetAll()

	// Define o cabeçalho de resposta como HTML UTF-8
	w.Header().Set("Content-Type", "text/html; charset=utf-8")

	// Renderiza o template "home.html", passando o slice de usuários como contexto
	if err := uc.Templates.ExecuteTemplate(w, "home.html", allUsers); err != nil {
		// Em caso de erro na renderização, retorna status 500
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

// AddUser trata tanto GET quanto POST em "/add":
// - GET: exibe o formulário (add.html)
// - POST: processa o formulário, adiciona o usuário e redireciona para "/"
func (uc *UserController) AddUser(w http.ResponseWriter, r *http.Request) {
	// Se for GET, apenas renderiza o formulário para adicionar usuário
	if r.Method == http.MethodGet {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		if err := uc.Templates.ExecuteTemplate(w, "add.html", nil); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}

	// Caso seja POST, processa os dados enviados pelo formulário
	if r.Method == http.MethodPost {
		// Faz o parse dos dados do form
		if err := r.ParseForm(); err != nil {
			http.Error(w, "Falha ao ler o formulário", http.StatusBadRequest)
			return
		}

		// Obtém os campos "name" e "email"
		name := r.FormValue("name")
		email := r.FormValue("email")

		// Cria um novo User e adiciona ao Model
		newUser := models.User{Name: name, Email: email}
		models.Add(newUser)

		// Após adicionar, redireciona para a rota "/" (lista de usuários)
		http.Redirect(w, r, "/", http.StatusSeeOther)
		return
	}

	// Se for outro método (PUT, DELETE etc.), retorna 405 Method Not Allowed
	http.Error(w, "Método não permitido", http.StatusMethodNotAllowed)
}

Sobre a estrutura principal, a função UserController carrega uma referência a *template.Template, que contém todos os nossos templates HTML (serão carregados em main.go).

Em seguida, com relação ao ListUsers, ele consulta a função models.GetAll(), que retorna um []User para serem usados posteriormente.

Por fim, temos o AddUser, que é responsável por fazer checagens (GET/POST) ler campos de formulários e renderizar o conteúdo na tela.

Passo 4) Crie o arquivo de rotas (routes.go)

Em routes.go, vamos centralizar o registro das rotas, fazendo com que cada caminho seja associado ao método correto do controller. Como não estamos usando bibliotecas externas de roteamento (mux, chi etc.), faremos tudo com net/http:

// routes.go
package main

import (
	"net/http"
	"padrao-mvc/controllers"
)

// SetupRoutes recebe a instância de UserController e associa cada rota ao handler correspondente
func SetupRoutes(uc *controllers.UserController) {
	// Rota principal "/" → lista de usuários
	http.HandleFunc("/", uc.ListUsers)

	// Rota "/add" → formulário e processamento de POST
	http.HandleFunc("/add", uc.AddUser)

	// (Opcional) servir arquivos estáticos caso você queira adicionar CSS, JS, imagens em /static/
//  fs := http.FileServer(http.Dir("public"))
//  http.Handle("/static/", http.StripPrefix("/static/", fs))
}

http.HandleFunc("/", uc.ListUsers) diz que toda requisição para http://localhost:3000/ (pela raiz) usará o método ListUsers.

http.HandleFunc("/add", uc.AddUser) indica que tanto GET /add quanto POST /add serão tratados em AddUser, que internamente verifica r.Method.

Se, mais adiante, você quiser servir arquivos estáticos dentro de public/ (CSS, imagens, JS), você pode "descomentar" as linhas correspondentes e criar uma pasta public/.

No nosso exemplo atual, não precisamos, então deixamos comentado para termos um pouco de clareza do nosso código 😊

Passo 5) Crie o ponto de entrada (main.go)

Já dentro do main.go, nós iremos:

  • Carregar todos os templates de uma só vez.
  • Instanciar o UserController, passando os templates.
  • Chamar SetupRoutes (de routes.go), passando o controller.
  • Iniciar o servidor HTTP na porta 3000.

Vamos nessa?

// main.go
package main

import (
	"html/template"
	"log"
	"net/http"
	"path/filepath"

	"padrao-mvc/controllers"
)

func main() {
	// 1. Carrega todos os templates dentro de "templates/*.html"
	//    O template.Must faz panic se algum arquivo estiver com erro de sintaxe
	tmpl := template.Must(template.ParseGlob(filepath.Join("templates", "*.html")))

	// 2. Cria o UserController, injetando a instância de templates
	userController := controllers.NewUserController(tmpl)

	// 3. Registra as rotas, associando cada caminho ao método do controller
	SetupRoutes(userController)

	// 4. Inicia o servidor na porta 3000
	log.Println("Servidor rodando em http://localhost:3000")
	if err := http.ListenAndServe(":3000", nil); err != nil {
		log.Fatal("Erro ao iniciar o servidor:", err)
	}
}

Passo 6) Crie os Templates HTML (templates/home.html e templates/add.html)

Já dentro da pasta de templates, precisamos criar dois arquivos HTML, primeiro o home.html:

<!-- templates/home.html -->
<!DOCTYPE html>
<html lang="pt-br">
<head>
    <meta charset="UTF-8" />
    <title>Lista de Usuários</title>
</head>
<body>
    <h1>Usuários Cadastrados</h1>

    <ul>
        {{- /* Itera sobre o slice de User passado pelo controller → contexto é []models.User */ -}}
        {{- range . }}
            <li>
                <strong>Nome:</strong> {{ .Name }}<br />
                <strong>Email:</strong> {{ .Email }}
            </li>
        {{- else }}
            <li><em>Nenhum usuário cadastrado.</em></li>
        {{- end }}
    </ul>

    <p>
        <a href="/add">Adicionar Novo Usuário</a>
    </p>
</body>
</html>

E segundo, o add.html:

<!-- templates/add.html -->
<!DOCTYPE html>
<html lang="pt-br">
<head>
    <meta charset="UTF-8" />
    <title>Adicionar Usuário</title>
</head>
<body>
    <h1>Adicionar Novo Usuário</h1>

    <form action="/add" method="post">
        <div>
            <label for="name">Nome:</label><br />
            <input type="text" id="name" name="name" required />
        </div>

        <div style="margin-top: 8px;">
            <label for="email">Email:</label><br />
            <input type="email" id="email" name="email" required />
        </div>

        <div style="margin-top: 12px;">
            <button type="submit">Salvar</button>
        </div>
    </form>

    <p>
        <a href="/">Voltar à Lista de Usuários</a>
    </p>
</body>
</html>

Passo 7) Execute um Mod Tidy

Por fim, vamos precisar executar o comando go mod tidy para baixar todas as dependências necessárias para executar o nosso projeto.

E pronto, se você fez tudo corretamente, terá a seguinte estrutura final do projeto:

padrão-mvc/                ← pasta raiz do nosso mini-projeto MVC
├── controllers/
│   └── user_controller.go
├── models/
│   └── user.go
├── templates/
│   ├── home.html
│   └── add.html
├── go.mod
├── go.sum        ← será gerado automaticamente ao baixar dependências
├── main.go
└── routes.go

Agora é só rodar a aplicação (go run .), acessar http://localhost:3000, e se certificar que tudo está funcionando corretamente.

Incrível, não acha? 😍

Padrão MVVM (Model-View-ViewModel)

O MVVM (Model-View-ViewModel) é um padrão geralmente associado a aplicações com interface gráfica (desktop ou mobile), em que:

Model: representa os dados e a lógica de negócio (igual ao MVC). Pode incluir structs, validações e acesso a banco.

View: é a camada de apresentação, responsável por exibir a interface ao usuário (ex.: botões, tabelas, campos de texto). Em Go, costuma-se usar frameworks como Fyne, Wails, Gio ou Walk para criar UIs nativas.

ViewModel: atua como adaptador entre Model e View. Contém propriedades “bindáveis” e comandos (funções) que a View “escuta” (data binding). Toda vez que o ViewModel é atualizado (por exemplo, Model mudou), a View reflete automaticamente as alterações.

Em linguagens como C# (WPF) ou Kotlin (Jetpack Compose), o MVVM é natural por conta de data-binding e recursos de “observables”.

Em Go, o mecanismo de data-binding não é inerente, pois o core não possui um sistema reativo nativo, mas alguns frameworks oferecem soluções semelhantes.

E sim, nativamente o padrão MVVM não é possível em Go, porém ele já conta com algumas bibliotecas que podem fazer esse trabalho por ele:

Fyne: é um toolkit multiplataforma para UIs em Go. Permite compilar aplicações para Windows, macOS, Linux, Android e iOS.

Wails: permite combinar Go (backend) com tecnologias web (HTML/CSS/JS) para criar UIs de desktop.

A ideia é que o “View” seja, em essência, uma página HTML renderizada em um WebView, gerida por frameworks JS (por exemplo, Vue, React ou Svelte).

Como nesta jornada a nossa intenção não é aprender criar aplicações para desktop em GoLang, recomendo que você aprenda o padrão MVVM de outras maneiras.

Outros padrões de arquitetura que podemos usar em Go

Além dos padrões MVC e MVVM, existem outras padrões que podemos aplicar em cada um de nossos projetos, vejamos cada um deles abaixo.

Clean Architecture (Arquitetura Limpa): Divide a aplicação em camadas concêntricas (Entidades, Casos de Uso, Adaptadores, Frameworks). As dependências apontam sempre para o interior, de modo que as regras de negócio (domínio) não conheçam detalhes de infraestrutura (banco, HTTP, UI).

Hexagonal Architecture (Ports and Adapters): Também chamada Arquitetura em “Portas e Adaptadores”. Separa o núcleo de regras de negócio (dentro) das interfaces externas (HTTP, banco, fila, e-mail) por meio de “ports” (interfaces) e “adapters” (implementações concretas).

Onion Architecture (Arquitetura em Cebola): Muito semelhante ao Clean/Hexagonal que organiza a aplicação em camadas circulares concêntricas, com o domínio no centro e camadas externas para interfaces e infraestrutura. A ideia é que cada “anel” só dependa dos que estão mais próximos do núcleo.

Domain-Driven Design (DDD): Escreve a interface web toda no cliente (por ex. React, Vue ou Go→WASM com Vugu/GopherJS), comportando-se como uma aplicação de página única. O servidor assume o papel de API REST. No caso de Go→WASM, o código Go transpila para WebAssembly e roda no navegador.

Visão geral das estruturas de pastas customizadas

É bem comum você encontrar pastas customizadas dentro de um determinado projeto em Go, alias, cada equipe de desenvolvimento pode trabalhar de forma diferente e tem seu próprio modus operandi na estruturação.

Hoje, eu venho trazer para você, algumas pastas que costumam ser bastante utilizadas em projetos em Go, tais como:

  • Pasta cmd/
  • Pasta Internal/
  • Pasta Config/

Vamos nessa? 😝

Entendendo a pasta CMD

Em alguns projetos feitos com GoLang, você pode encontrar uma pasta dentro da pasta principal do projeto chamada de cmd.

Ela é comumente usada para colocar todos os arquivos em Go que são executáveis (entry points) dentro dessa pasta.

Além disso, podem existir subpastas como cmd/web/ que contém a inicialização do servidor HTTP (conexão a banco, criação de router, injeção de dependências, configuração de middlewares, etc.). Ou seja, tudo o que é necessário para “levantar” a aplicação em modo web ficará dentro dessa pasta.

Portanto, dentro da pasta cmd costumamos inserir arquivos que possuem as seguintes responsabilidades:

  • Carregar variáveis de ambiente e configurações (usando pacote config).
  • Instanciar clientes externos e serviços (por exemplo, conexões gRPC, instâncias de repositório, clientes HTTP).
  • Configurar o framework web (neste caso, Labstack Echo).
  • Registrar rotas e middlewares globais (logging, autenticação, observabilidade).
  • Iniciar o servidor (ex.: echo.Start() ou echo.Logger.Fatal(echo.StartTLS(…))).

Quando você separa claramente o “como iniciar a aplicação” do restante da lógica de negócio, você acaba facilitando testes unitários no interior sem precisar rodar o servidor como um todo 😌

Entendendo a pasta Internal

Em Go, o diretório internal/ funciona de forma especial, onde os pacotes dentro dele só podem ser importados por códigos que esteja no mesmo módulo ou em subdiretórios. Isso garante que ninguém de fora use APIs consideradas “privadas”.

Dentro de internal/, podemos encontrar as seguintes subpastas:

internal/
├── http/        ← handlers e controllers HTTP
├── services/    ← regras de negócio, orquestração de repositórios
├── repositories/← implementação de persistência (Postgres, Redis etc.)
├── middlewares/ ← interceptadores (autenticação, autorização, CORS, logs)
└── domain/      ← entidades e interfaces (casos de uso)

Geralmente dentro da subpasta http, podemos ter outras pasta como:

auth: contém handlers e páginas relacionadas a autenticação (login, logout, registro, verificação de token, inclusão de HTMX endpoints, etc.).

Na maioria das vezes, os arquivos aqui dentro lidam com o middleware de JWT via github.com/golang-jwt/jwt/v5.

clients: contém clientes HTTP para comunicar-se com APIs externas (por exemplo, Canva Adapter, TLMTR, ou qualquer outro serviço).

Entendendo a pasta Config

Já em outras projetos em Go, você pode encontrar alguns arquivos que armazenam as configurações — geralmente YAML, JSON ou variáveis de ambiente — que determinam como a aplicação se comporta em diferentes ambientes (desenvolvimento, homologação, produção).

E é claro, dentro dessa pasta config/, pode existir varias subpastas, tais como a locales/, que contém arquivos de internacionalização (usando o i18n), no formato JSON ou TOML, usados por go-i18n para traduzir textos da aplicação (mensagens de erro, labels, templates).

Criando um projeto completo usando a lógica de cada uma dessas pastas

Para que possamos entender melhor, que tal criar um projeto bem completo fazendo o uso das pastas cmd/ e internal/?

Neste exemplo, ainda vamos usar aquele mesmo projeto que criamos anteriormente no tópico sobre MVC, a diferença é que iremos estrutura-lo de outra forma ☺️

Tudo começa com a criação de uma nova pasta dentro de 24-criando-servidores-robustos-em-go chamada de simpleapp, que vai conter toda essa estrutura de arquivos:

simpleapp/                          ← pasta‐raiz do projeto
├── cmd
│   └── web
│       └── main.go                 ← entry point: carrega dependências e inicia servidor
├── internal
│   ├── domain
│   │   └── user.go                ← entidade User e interface UserRepository
│   ├── repositories
│   │   └── user_inmemory.go       ← implementação in‐memory de UserRepository
│   ├── services
│   │   └── user_service.go        ← UserService: chama UserRepository e aplica regras
│   ├── http
│   │   └── handlers
│   │       └── user_handler.go     ← HTTP Handlers (List e Add)
│   └── middlewares
│       └── logging.go             ← Exemplo de middleware para log simples
├── templates
│   ├── home.html                  ← mostra a lista de usuários
│   └── add.html                   ← formulário para adicionar usuário
├── go.mod                         
└── go.sum                         ← gerado automaticamente

Sendo assim, não se esqueça de criar todas essas subpastas com seus respectivos arquivos, antes de continuar, ok?

Há... além disso, é bem provável que você tenha que inicializar um novo projeto em GoLang chamado simpleapp:

go mod init simpleapp

Feito isso, vamos nos concentrar agora na lógica existente em cada um dos arquivos 😉

main.go:

// cmd/web/main.go
package main

import (
	"html/template"
	"log"
	"net/http"
	"path/filepath"

	"simpleapp/internal/http/handlers"
	"simpleapp/internal/middlewares"
	"simpleapp/internal/repositories"
	"simpleapp/internal/services"
)

func main() {
	// 1. Carrega todos os templates (*.html) no diretório "templates"
	tmpl := template.Must(template.ParseGlob(filepath.Join("templates", "*.html")))

	// 2. Instancia o repositório in‐memory e o serviço
	userRepo := repositories.NewUserInMemory()
	userSvc := services.NewUserService(userRepo)

	// 3. Cria o handler, injetando o serviço e templates
	userHandler := handlers.NewUserHandler(userSvc, tmpl)

	// 4. Configura o mux e registra rotas com middleware de logging
	mux := http.NewServeMux()

	// Middleware de logging: LoggingMiddleware( handler )
	mux.Handle("/", middlewares.LoggingMiddleware(http.HandlerFunc(userHandler.List)))
	mux.Handle("/add", middlewares.LoggingMiddleware(http.HandlerFunc(userHandler.Add)))

	// 5. Inicia o servidor
	addr := ":3000"
	log.Println("Servidor rodando em http://localhost" + addr)
	if err := http.ListenAndServe(addr, mux); err != nil {
		log.Fatal("Erro ao iniciar servidor:", err)
	}
}

user.go:

// internal/domain/user.go
package domain

// User representa um usuário no sistema.
type User struct {
	Name  string
	Email string
}

// UserRepository define a interface para persistir/recuperar Users.
// É o “port” na Hexagonal/Clean Architecture.
type UserRepository interface {
	// GetAll retorna todos os usuários cadastrados.
	GetAll() ([]User, error)
	// Add persiste um novo usuário.
	Add(u User) error
}

user_handler.go:

// internal/http/handlers/user_handler.go
package handlers

import (
	"html/template"
	"net/http"

	"simpleapp/internal/services"
)

// UserHandler contém dependências para atender requisições relacionadas a User.
type UserHandler struct {
	Service   *services.UserService
	Templates *template.Template
}

// NewUserHandler recebe a instância de service e templates já carregados.
func NewUserHandler(svc *services.UserService, tmpl *template.Template) *UserHandler {
	return &UserHandler{Service: svc, Templates: tmpl}
}

// List exibe a página com lista de usuários (GET "/").
func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) {
	// Chama o service para obter todos os usuários
	users, err := h.Service.ListUsers()
	if err != nil {
		http.Error(w, "Erro ao obter usuários: "+err.Error(), http.StatusInternalServerError)
		return
	}

	// Define cabeçalho
	w.Header().Set("Content-Type", "text/html; charset=utf-8")

	// Renderiza o template home.html, passando o slice de domain.User como contexto
	if err := h.Templates.ExecuteTemplate(w, "home.html", users); err != nil {
		http.Error(w, "Erro ao renderizar template: "+err.Error(), http.StatusInternalServerError)
		return
	}
}

// Add exibe o formulário (GET "/add") e processa submissão (POST "/add").
func (h *UserHandler) Add(w http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodGet {
		// Renderiza o formulário
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		if err := h.Templates.ExecuteTemplate(w, "add.html", nil); err != nil {
			http.Error(w, "Erro ao renderizar template: "+err.Error(), http.StatusInternalServerError)
		}
		return
	}

	if r.Method == http.MethodPost {
		// Processa os dados do form
		if err := r.ParseForm(); err != nil {
			http.Error(w, "Falha ao ler formulário: "+err.Error(), http.StatusBadRequest)
			return
		}
		name := r.FormValue("name")
		email := r.FormValue("email")

		// Chama o serviço para criar o usuário
		if err := h.Service.CreateUser(name, email); err != nil {
			// Em caso de erro (nome vazio, e‐mail vazio ou e‐mail duplicado), retornamos ao form com mensagem
			http.Error(w, "Falha ao adicionar usuário: "+err.Error(), http.StatusBadRequest)
			return
		}

		// Sucesso → redireciona para List ("/")
		http.Redirect(w, r, "/", http.StatusSeeOther)
		return
	}

	// Se for outro método, devolve 405
	http.Error(w, "Método não permitido", http.StatusMethodNotAllowed)
}

logging.go:

// internal/middlewares/logging.go
package middlewares

import (
	"log"
	"net/http"
	"time"
)

// LoggingMiddleware grava no console o método, path e tempo de execução de cada requisição.
func LoggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		// Chama o próximo handler na cadeia
		next.ServeHTTP(w, r)
		// Após a resposta, registra dados
		duration := time.Since(start)
		log.Printf("%s %s → %v\n", r.Method, r.URL.Path, duration)
	})
}

user_inmemory.go:

// internal/repositories/user_inmemory.go
package repositories

import (
	"fmt"
	"sync"

	"simpleapp/internal/domain"
)

// userInMemory é a struct que implementa domain.UserRepository usando um slice em memória.
// Protegemos o slice com um mutex para concorrência.
type userInMemory struct {
	mu    sync.RWMutex
	store []domain.User
}

// NewUserInMemory cria uma instância de UserRepository em memória.
func NewUserInMemory() domain.UserRepository {
	return &userInMemory{
		store: make([]domain.User, 0),
	}
}

// GetAll retorna todos os usuários. Bloqueia mutex para leitura.
func (r *userInMemory) GetAll() ([]domain.User, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()

	// Retorna uma cópia do slice para evitar que chamador manipule diretamente o slice interno
	copySlice := make([]domain.User, len(r.store))
	copy(copySlice, r.store)
	return copySlice, nil
}

// Add adiciona um novo usuário. Bloqueia mutex para escrita.
func (r *userInMemory) Add(u domain.User) error {
	r.mu.Lock()
	defer r.mu.Unlock()

	// Poderíamos, por exemplo, checar se já existe um e-mail igual:
	for _, existing := range r.store {
		if existing.Email == u.Email {
			return fmt.Errorf("usuário com e-mail %s já existe", u.Email)
		}
	}

	r.store = append(r.store, u)
	return nil
}

user_service.go:

// internal/services/user_service.go
package services

import (
	"errors"
	"strings"

	"simpleapp/internal/domain"
)

// UserService orquestra o uso de domain.UserRepository e aplica validações.
type UserService struct {
	Repo domain.UserRepository
}

// NewUserService cria uma instância de UserService com o repositório injetado.
func NewUserService(repo domain.UserRepository) *UserService {
	return &UserService{Repo: repo}
}

// ListUsers retorna todos os usuários.
// Propaga qualquer erro de Repo.GetAll.
func (s *UserService) ListUsers() ([]domain.User, error) {
	return s.Repo.GetAll()
}

// CreateUser valida os dados e, se estiverem corretos, adiciona ao repositório.
// Retorna erro caso o e-mail esteja vazio, formato inválido, ou se Repo.Add falhar.
func (s *UserService) CreateUser(name, email string) error {
	// Validações básicas:
	name = strings.TrimSpace(name)
	email = strings.TrimSpace(email)

	if name == "" {
		return errors.New("nome é obrigatório")
	}
	if email == "" {
		return errors.New("email é obrigatório")
	}
	// Poderíamos adicionar validação de formato de e-mail aqui (regex), mas mantemos simples.

	// Cria entidade de domínio e persiste
	u := domain.User{Name: name, Email: email}
	return s.Repo.Add(u)
}

add.html:

<!-- templates/add.html -->
<!DOCTYPE html>
<html lang="pt-br">
<head>
    <meta charset="UTF-8" />
    <title>Adicionar Usuário</title>
</head>
<body>
    <h1>Adicionar Novo Usuário</h1>

    <form action="/add" method="post">
        <div>
            <label for="name">Nome:</label><br />
            <input type="text" id="name" name="name" required />
        </div>

        <div style="margin-top: 8px;">
            <label for="email">Email:</label><br />
            <input type="email" id="email" name="email" required />
        </div>

        <div style="margin-top: 12px;">
            <button type="submit">Salvar</button>
        </div>
    </form>

    <p>
        <a href="/">Voltar à Lista de Usuários</a>
    </p>
</body>
</html>

home.html:

<!-- templates/home.html -->
<!DOCTYPE html>
<html lang="pt-br">
<head>
    <meta charset="UTF-8" />
    <title>Lista de Usuários</title>
</head>
<body>
    <h1>Usuários Cadastrados</h1>

    <ul>
        {{- range . }}
            <li>
                <strong>Nome:</strong> {{ .Name }}<br />
                <strong>Email:</strong> {{ .Email }}
            </li>
        {{- else }}
            <li><em>Nenhum usuário cadastrado.</em></li>
        {{- end }}
    </ul>

    <p>
        <a href="/add">Adicionar Novo Usuário</a>
    </p>
</body>
</html>

Por fim, não se esqueça de executar o go mod tidy antes de executar o projeto: go run cmd/web/main.go.

A estrutura da tela em si, e o funcionamento da lógica de adição de novos usuários, não mudou, com exceção dos novos logs que são mostrados no console:

A partir de agora, você sabe quais rotas estão sendo chamadas, e o tempo de execução de cada uma delas 🙂

E além disso, seu projeto ganhou novas camadas de complexidade, que o deixou bem completo!

Repositório da lição

Todos os arquivos relacionados com esta lição, podem ser encontrados nos seguintes repositórios abaixo:

Conclusão

Nesta lição, você aprendeu a criar servidores mais robustos em conjunto com diversos padrões de arquitetura.

Até a próxima lição 😆

Criadores de Conteúdo

Foto do William Lima
William Lima
Fundador da Micilini

Inventor nato, escreve conteudos de programação para o portal da micilini.

Torne-se um MIC 🤖

Mais de 100 mic's já estão conectados na plataforma.