Go: Arquitetura do Projeto Go
Última atualização: 2026-08-26
O Go não possui uma estrutura de projeto imposta por um framework — mas isso significa que você mesmo precisa projetá-la. Uma arquitetura bem planejada mantém seu projeto claro quando ele tem 1.000 linhas e ainda assim gerenciável quando chega a 100.000 linhas.
Quando você assume um projeto em que todo o código está amontoado em main.go, o primeiro passo na refatoração é mover o código para os diretórios corretos.
1. Você aprenderá
- Estrutura padrão de diretórios de um projeto em Go:
cmd/,internal/,pkg/ - O modelo de 4 camadas da Arquitetura Limpa
- Injeção de dependências (injeção no construtor)
- Definição de tipo incorreta
- Definição da estrutura do projeto
2. A história real de um engenheiro de backend
(1) Problema: 100.000 linhas de código estão todas no arquivo main.go
Alice assumiu a coordenação de um projeto de comércio eletrônico:
“Meu antecessor colocou todo o código em
main.go: registro de rotas, operações de banco de dados, modelos HTML e lógica de negócios — tudo misturado em 30.000 linhas. Quando eu quis adicionar um campo de status do usuário, tive que fazer alterações em cinco lugares diferentes para que funcionasse corretamente. Levei duas semanas para adicionar um recurso e três dias só para encontrar o código.”
// Bad code: everything piled into main.go
package main
var db *sql.DB
func main() {
// Database connection
// Route registration
// HTML templates
// User handler
// Order handler
// Product handler
// All mixed together!
}
// To find "user" related code: Ctrl+F search for "user", scattered across 50 places
(2) Solução do Go: Arquitetura de 4 camadas
myapp/
├── cmd/
│ └── server/
│ └── main.go # Entry → dependency injection + start service
├── internal/
│ ├── handler/ # Layer 1: HTTP handlers (parse request / return response)
│ ├── service/ # Layer 2: Business logic (domain rules)
│ └── repository/ # Layer 3: Data access (database / external API)
├── pkg/
│ └── model/ # Layer 4: Domain models (data structures)
└── go.mod
(3) Retornos: Caos x Estratificação
| Cenário | Arquitetura caótica | Arquitetura de 4 camadas |
|---|---|---|
| Novos campos | 5 alterações em locais desconhecidos | Alterações apenas no modelo e no manipulador |
| Alternar entre bancos de dados | Atualizar todas as chamadas ao banco de dados | Atualizar apenas a camada de repositório |
| Testes de unidade | Não é possível realizar testes de unidade | Cada camada pode ser testada de forma independente usando simulações |
| Introdução para iniciantes | Navegando pelas 30.000 linhas do arquivo main.go | Compreendendo as responsabilidades apenas observando os nomes dos diretórios |
3. Layout padrão do projeto
(1) Uma explicação detalhada da estrutura de diretórios
myproject/
├── cmd/ # Executable entry points
│ ├── server/ # server binary
│ │ └── main.go
│ └── migrate/ # database migration tool
│ └── main.go
├── internal/ # Private packages (cannot be imported externally)
│ ├── handler/ # HTTP handlers
│ ├── service/ # Business logic
│ └── repository/ # Data access
├── pkg/ # Exportable public packages
│ └── model/ # Domain models
├── migrations/ # SQL migration files
├── config/ # Configuration files
├── scripts/ # Helper scripts
├── go.mod
└── go.sum
(2) cmd/ / internal/ / pkg/ Responsabilidades
| Diretório | Usos | Pode ser importado externamente |
|---|---|---|
cmd/ |
Ponto de entrada do executável (função principal) | N/A (é o pacote principal) |
internal/ |
Implementação privada | ❌ As importações externas são proibidas pelo compilador Go |
pkg/ |
Código público exposto ao mundo exterior | ✅ |
internal é um diretório especial para o compilador Go — nenhum pacote fora do diretório pai de internal pode importá-lo. Isso proporciona um verdadeiro encapsulamento, que é mais seguro do que as “convenções de nomenclatura” (como _private).
4. O modelo de quatro camadas da Arquitetura Limpa
handler (HTTP) → service (business) → repository (data)
↓ ↓ ↓
Request parsing Domain rules Database/API
Response return Transaction mgmt CRUD operations
Param validation Multi-step orchestration Cache access
(1) ▶ Exemplo: implementação em 4 camadas
// ---------- Layer 4: Model (domain models) ----------
// pkg/model/user.go
package model
type User struct {
ID int
Name string
Email string
CreatedAt time.Time
}
type CreateUserRequest struct {
Name string
Email string
}
// ---------- Layer 3: Repository (data access) ----------
// internal/repository/user.go
package repository
import (
"database/sql"
"myapp/pkg/model"
)
type UserRepository interface {
FindByID(id int) (*model.User, error)
Create(req model.CreateUserRequest) (*model.User, error)
List() ([]*model.User, error)
Delete(id int) error
}
type userRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) FindByID(id int) (*model.User, error) {
row := r.db.QueryRow("SELECT id, name, email, created_at FROM users WHERE id = ?", id)
user := &model.User{}
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
return user, err
}
func (r *userRepository) Create(req model.CreateUserRequest) (*model.User, error) {
result, err := r.db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", req.Name, req.Email)
if err != nil {
return nil, err
}
id, _ := result.LastInsertId()
return r.FindByID(int(id))
}
// ---------- Layer 2: Service (business logic) ----------
// internal/service/user.go
package service
import (
"errors"
"myapp/internal/repository"
"myapp/pkg/model"
"strings"
)
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidName = errors.New("name is required")
ErrInvalidEmail = errors.New("invalid email format")
)
type UserService struct {
repo repository.UserRepository
}
func NewUserService(repo repository.UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) Create(req model.CreateUserRequest) (*model.User, error) {
if strings.TrimSpace(req.Name) == "" {
return nil, ErrInvalidName
}
if !strings.Contains(req.Email, "@") {
return nil, ErrInvalidEmail
}
return s.repo.Create(req)
}
func (s *UserService) Get(id int) (*model.User, error) {
user, err := s.repo.FindByID(id)
if err != nil {
return nil, err
}
if user == nil {
return nil, ErrUserNotFound
}
return user, nil
}
// ---------- Layer 1: Handler (HTTP handler) ----------
// internal/handler/user.go
package handler
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"myapp/internal/service"
"myapp/pkg/model"
)
type UserHandler struct {
svc *service.UserService
}
func NewUserHandler(svc *service.UserService) *UserHandler {
return &UserHandler{svc: svc}
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req model.CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON")
return
}
user, err := h.svc.Create(req)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusCreated, user)
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("id"))
user, err := h.svc.Get(id)
if errors.Is(err, service.ErrUserNotFound) {
writeError(w, http.StatusNotFound, err.Error())
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "internal error")
return
}
writeJSON(w, http.StatusOK, user)
}
// ---------- Utility functions ----------
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}
(2) Direção da dependência
Handler → Service → Repository (→ DB)
| | |
↓ ↓ ↓
Model Model Model
| Camada | Responsabilidades | Dependências | Testável |
|---|---|---|---|
| Handler | Análise/Resposta HTTP | Serviço | Serviço simulado |
| Serviço | Regras de Negócio/Orquestração | Repositório | Repositório Simulado |
| Repositório | Operações CRUD em dados | banco de dados/SQL | Banco de dados simulado / Testes de integração |
| Modelo | Estrutura de dados | Nenhuma | — |
5. Injeção de dependências
(1) ▶ Exemplo: Injeção de construtor
⚙️ Pré-requisito: Execute
go get github.com/mattn/go-sqlite3(requer CGO; como alternativa, usemodernc.org/sqlitepara um driver exclusivamente de Go)
// cmd/server/main.go
package main
import (
"database/sql"
"log"
"net/http"
"myapp/internal/handler"
"myapp/internal/repository"
"myapp/internal/service"
)
func main() {
// ---- Dependency injection (assemble all layers) ----
// 1. Database connection
db, err := sql.Open("sqlite3", "./app.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 2. Repository layer
userRepo := repository.NewUserRepository(db)
orderRepo := repository.NewOrderRepository(db)
// 3. Service layer (depends on Repository)
userSvc := service.NewUserService(userRepo)
orderSvc := service.NewOrderService(orderRepo, userRepo)
// 4. Handler layer (depends on Service)
userHandler := handler.NewUserHandler(userSvc)
orderHandler := handler.NewOrderHandler(orderSvc)
// 5. Route registration
mux := http.NewServeMux()
userHandler.Register(mux, "/api/v1/users")
orderHandler.Register(mux, "/api/v1/orders")
log.Println("Service listening on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
(2) Comparação entre abordagens de injeção de dependências
| Método | Implementação | Vantagens | Desvantagens |
|---|---|---|---|
| Injeção de construtor | NewXxx(dep) |
Verificação explícita em tempo de compilação | Código extenso devido às dependências |
| Implementação manual | Montagem da função principal | Não requer bibliotecas de terceiros | Difícil de manter em projetos de grande porte |
| Google Wire | Geração de código | Injeção automática | Curva de aprendizado |
github.com/google/wire) para gerar automaticamente o código de injeção de dependências.
6. Exemplo completo: Estrutura de um projeto de comércio eletrônico
(1) ▶ Exemplo: Implementação completa
// cmd/server/main.go
package main
import (
"context"
"database/sql"
"encoding/json"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
_ "github.com/mattn/go-sqlite3"
)
// ---------- Model (pkg/model) ----------
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
type Order struct {
ID int `json:"id"`
UserID int `json:"user_id"`
ProductID int `json:"product_id"`
Quantity int `json:"quantity"`
Total float64 `json:"total"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
// ---------- Repository (internal/repository) ----------
type ProductRepository struct {
db *sql.DB
}
func NewProductRepository(db *sql.DB) *ProductRepository {
return &ProductRepository{db: db}
}
func (r *ProductRepository) FindByID(id int) (*Product, error) {
p := &Product{}
err := r.db.QueryRow("SELECT id, name, price FROM products WHERE id = ?", id).
Scan(&p.ID, &p.Name, &p.Price)
if err == sql.ErrNoRows {
return nil, nil
}
return p, err
}
func (r *ProductRepository) List() ([]*Product, error) {
rows, err := r.db.Query("SELECT id, name, price FROM products")
if err != nil {
return nil, err
}
defer rows.Close()
var products []*Product
for rows.Next() {
p := &Product{}
if err := rows.Scan(&p.ID, &p.Name, &p.Price); err != nil {
return nil, err
}
products = append(products, p)
}
return products, rows.Err()
}
type OrderRepository struct {
db *sql.DB
}
func NewOrderRepository(db *sql.DB) *OrderRepository {
return &OrderRepository{db: db}
}
func (r *OrderRepository) Create(order *Order) error {
result, err := r.db.Exec(
"INSERT INTO orders (user_id, product_id, quantity, total, status) VALUES (?, ?, ?, ?, ?)",
order.UserID, order.ProductID, order.Quantity, order.Total, order.Status,
)
if err != nil {
return err
}
id, _ := result.LastInsertId()
order.ID = int(id)
return nil
}
func (r *OrderRepository) FindByID(id int) (*Order, error) {
o := &Order{}
err := r.db.QueryRow(
"SELECT id, user_id, product_id, quantity, total, status, created_at FROM orders WHERE id = ?", id,
).Scan(&o.ID, &o.UserID, &o.ProductID, &o.Quantity, &o.Total, &o.Status, &o.CreatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
return o, err
}
// ---------- Service (internal/service) ----------
type OrderService struct {
productRepo *ProductRepository
orderRepo *OrderRepository
}
func NewOrderService(productRepo *ProductRepository, orderRepo *OrderRepository) *OrderService {
return &OrderService{
productRepo: productRepo,
orderRepo: orderRepo,
}
}
type PlaceOrderInput struct {
UserID int
ProductID int
Quantity int
}
var (
ErrProductNotFound = &AppError{Code: "PRODUCT_NOT_FOUND", Message: "product not found", HTTPStatus: 404}
ErrInsufficientStock = &AppError{Code: "INSUFFICIENT_STOCK", Message: "insufficient stock", HTTPStatus: 409}
)
type AppError struct {
Code string `json:"code"`
Message string `json:"message"`
HTTPStatus int `json:"-"`
}
func (e *AppError) Error() string {
return e.Message
}
func (s *OrderService) PlaceOrder(input PlaceOrderInput) (*Order, error) {
product, err := s.productRepo.FindByID(input.ProductID)
if err != nil {
return nil, err
}
if product == nil {
return nil, ErrProductNotFound
}
total := product.Price * float64(input.Quantity)
order := &Order{
UserID: input.UserID,
ProductID: input.ProductID,
Quantity: input.Quantity,
Total: total,
Status: "created",
}
if err := s.orderRepo.Create(order); err != nil {
return nil, err
}
return order, nil
}
// ---------- Handler (internal/handler) ----------
type OrderHandler struct {
svc *OrderService
}
func NewOrderHandler(svc *OrderService) *OrderHandler {
return &OrderHandler{svc: svc}
}
func (h *OrderHandler) Register(mux *http.ServeMux, basePath string) {
mux.HandleFunc("POST "+basePath, h.PlaceOrder)
mux.HandleFunc("GET "+basePath+"/{id}", h.GetOrder)
}
type placeOrderRequest struct {
ProductID int `json:"product_id"`
Quantity int `json:"quantity"`
}
func (h *OrderHandler) PlaceOrder(w http.ResponseWriter, r *http.Request) {
// Get user ID from Context (injected by auth middleware)
userID, ok := r.Context().Value("user_id").(int)
if !ok {
writeError(w, http.StatusUnauthorized, "not authenticated")
return
}
var req placeOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON")
return
}
order, err := h.svc.PlaceOrder(PlaceOrderInput{
UserID: userID,
ProductID: req.ProductID,
Quantity: req.Quantity,
})
if appErr, ok := err.(*AppError); ok {
writeError(w, appErr.HTTPStatus, appErr.Message)
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "internal error")
return
}
writeJSON(w, http.StatusCreated, order)
}
func (h *OrderHandler) GetOrder(w http.ResponseWriter, r *http.Request) {
// ... business logic
}
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}
// ---------- Main (dependency injection entry) ----------
func main() {
db, err := sql.Open("sqlite3", "./shop.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Initialize tables
db.Exec(`CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, price REAL
)`)
db.Exec(`CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER,
product_id INTEGER, quantity INTEGER, total REAL,
status TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`)
// Dependency injection
productRepo := NewProductRepository(db)
orderRepo := NewOrderRepository(db)
orderSvc := NewOrderService(productRepo, orderRepo)
orderHandler := NewOrderHandler(orderSvc)
mux := http.NewServeMux()
orderHandler.Register(mux, "/api/v1/orders")
// Health check
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Graceful shutdown
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("Shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
server.Shutdown(ctx)
}()
log.Println("E-commerce service listening on :8080")
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}
graph TB
subgraph Handler
H[HTTP Handler]
end
subgraph Service
S[Business Logic]
end
subgraph Repository
R[Data Access]
end
subgraph Model
M[Domain Structs]
end
subgraph External ["External Dependencies"]
DB[(Database)]
end
H -->|calls| S
S -->|calls| R
R -->|queries| DB
H --- M
S --- M
R --- M
style Handler fill:#e1f5fe
style Service fill:#fff3e0
style Repository fill:#e8f5e9
style Model fill:#f3e5f5
❓ Perguntas Frequentes
P: O que é a Arquitetura Limpa? R: Um padrão arquitetônico em camadas — de fora para dentro: Handler (Camada de Interface) → Serviço (Camada de Caso de Uso) → Repositório (Camada de Dados) → Modelo (Camada de Domínio). Princípio fundamental: as dependências fluem das camadas externas para as internas; as camadas internas não têm conhecimento da existência das camadas externas. As camadas externas implementam interfaces, enquanto as camadas internas definem interfaces.
P: Como as camadas do modelo de 4 camadas são organizadas? R: (1) Handler: análise e resposta HTTP, validação de parâmetros; (2) Serviço: regras de negócios, orquestração em várias etapas, transações; (3) Repositório: operações CRUD no banco de dados, acesso ao cache, APIs externas; (4) Modelo: definições de estruturas de dados. Cada camada depende apenas da camada abaixo dela e é desacoplada por meio de interfaces.
P: Como a injeção de dependências é implementada? R: A injeção por construtor é a abordagem mais comum:
NewXxx(dep1, dep2) *Xxx. Todas as dependências são criadas e montadas na função principal (ou no código gerado pelo Wire). Vantagens: (1) Relações de dependência claras; (2) Verificações em tempo de compilação; (3) Capacidade de passar simulações durante os testes unitários.
P: Por que usar o diretório
internal? R: O compilador Go garante que os pacotes no diretóriointernalsó possam ser importados por código localizado no diretório pai. Isso proporciona um verdadeiro encapsulamento — usuários externos não podem importar pacotes dointernal. Em projetos grandes, isso evita violações de arquitetura (comohandlersser importado diretamente pararepository).
P: Como os tipos de erro devem ser definidos? R: Defina tipos de erro personalizados que incluam os campos Código, Mensagem e HTTPStatus. A camada de Serviço retorna erros de negócios (como ErrProductNotFound), e a camada de Manipulação mapeia esses erros para códigos de status HTTP e respostas JSON. Benefícios: um formato unificado de erros torna os tipos de erro imediatamente claros.
P: É necessário usar interfaces entre camadas? R: É recomendado. As interfaces implícitas do Go fazem com que a inversão de dependências pareça natural — o Repository define uma interface, o Handler depende dessa interface e a implementação concreta é injetada por meio do construtor. Benefícios: (1) Os testes unitários podem usar simulações; (2) A troca de implementações (por exemplo, SQLite → MySQL) não requer alterações no chamador.
P: Os projetos pequenos também precisam de uma arquitetura de 4 camadas? R: Não. A escala do projeto determina a profundidade da arquitetura — use uma estrutura plana para projetos com menos de 500 linhas de código, uma estrutura de 2 camadas (handler + repositório) para projetos com 500 a 5.000 linhas e uma estrutura de 4 camadas para projetos com mais de 5.000 linhas. Recomenda-se começar com uma arquitetura de duas camadas e adicionar camadas gradualmente à medida que o projeto cresce. Evite o excesso de engenharia.
📖 Resumo
- Layout padrão:
cmd/(ponto de entrada),internal/(privado),pkg/(público) - Modelo de 4 camadas: Handler → Serviço → Repositório → Modelo
- Direção da dependência: da camada externa para a interna; a camada interna não tem conhecimento da camada externa
- Injeção de dependências: injeção por construtor (a mais comum)
- Desacoplamento de interfaces: as interfaces implícitas do Go tornam a inversão de dependências algo natural
- Tipo de erro: AppError personalizado contendo Código/Mensagem/HTTPStatus
- Diretório
internal: Encapsulamento imposto pelo compilador Go
📝 Exercícios
-
Básico (Dificuldade ⭐): Crie uma estrutura básica do projeto contendo os seguintes diretórios: cmd/server/main.go, internal/handler/, internal/service/, internal/repository/ e pkg/model/. Implemente um
HealthHandlersimples que retorne{"status": "ok"}. -
Avançado (Dificuldade ⭐⭐): Implemente o Gerenciamento de Categorias de Produtos utilizando uma arquitetura de 4 camadas. Requisitos: (1) Modelo: Category(id, name, parent_id); (2) Handler: endpoints CRUD; (3) Serviço: Validar se os nomes das categorias são únicos e evitar referências circulares; (4) Repositório: Implementado usando SQLite; (5) A função
mainmonta os componentes por meio de injeção de construtor. -
Desafio (Dificuldade ⭐⭐⭐): Refatore o sistema de gerenciamento de bibliotecas da Lição 22 para uma arquitetura de quatro camadas. Requisitos: (1) Mova o código para o diretório
cmd/internal/pkg; (2) A camada Handler deve lidar apenas com HTTP; (3) A camada Service deve incluir validação completa dos negócios; (4) A camada Repository deve usar o SQLite; (5) Exponha o Repository como uma interface para permitir testes com simulações; (6) Escreva testes de unidade (usando um repositório simulado) para testar as regras de negócios na camada Service.