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á


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.”

GO
// 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

TEXT 📖 Somente leitura
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

TEXT 📖 Somente leitura
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
💡 Dica: O diretório 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

TEXT 📖 Somente leitura
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

GO 📖 Somente leitura
// ---------- 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})
}
131 linhas de lógica (limite de 40, somente leitura)

(2) Direção da dependência

TEXT 📖 Somente leitura
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
🔥 Erro comum: As dependências devem fluir da camada externa para a camada interna. O Handler conhece o Service, e o Service conhece o Repository, mas o Repository não deve conhecer o Service. Cada camada depende apenas da camada abaixo dela, e a desacoplagem é alcançada por meio de interfaces (as interfaces implícitas do Go fazem com que isso pareça natural).


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, use modernc.org/sqlite para um driver exclusivamente de Go)

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))
}
▶ Experimente

(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
💡 Dica: A injeção manual de dependências é suficiente para projetos pequenos. Quando seu projeto tiver 20 ou mais serviços e 50 ou mais dependências, considere usar o Google Wire (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

GO 📖 Somente leitura
// 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)
    }
}
225 linhas de lógica (limite de 40, somente leitura)
100%
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
🔥 Erro comum: Não pule nenhuma camada. Mesmo que seu Serviço chame apenas um único método no Repositório, você deve passar pela camada de Serviço — as regras de negócios se expandirão no futuro. Pular camadas fará com que a lógica de negócios fique espalhada pelo Handler, tornando impossível realizar testes unitários.


❓ 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ório internal só possam ser importados por código localizado no diretório pai. Isso proporciona um verdadeiro encapsulamento — usuários externos não podem importar pacotes do internal. Em projetos grandes, isso evita violações de arquitetura (como handlers ser importado diretamente para repository).

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


📝 Exercícios

  1. 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 HealthHandler simples que retorne {"status": "ok"}.

  2. 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 main monta os componentes por meio de injeção de construtor.

  3. 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%