Neste post, vamos explorar como organizar um projeto em Go. Diferente de outras linguagens, onde frameworks geralmente oferecem padrões de organização de código, em Go temos liberdade para organizar o projeto da maneira que preferirmos. Isso é bom, mas sem um padrão claro, o código pode se tornar desorganizado e difícil de manter. Por isso, é essencial estabelecer uma estrutura que permita aos desenvolvedores localizar facilmente onde cada parte do código deve ser colocada.
Nessa estrutura vamos conseguir aplicar os princípios de Clean Architecture e usar as vantagens da linguagem Go para criar um projeto bem organizado e fácil de manter. Vale ressaltar que a estrutura deve mudar de acordo com as necessidades de cada projeto, então é importante adaptar para o contexto específico.
Go é uma linguagem organizada em pacotes, e por isso é fundamental pensar na estrutura do projeto de forma que os pacotes sejam bem definidos e de fácil entendimento. Algumas diretrizes ajudam a manter a organização e a qualidade do código:
- Isolamento de Responsabilidade: Cada pacote deve ter uma responsabilidade bem definida, evitando misturar funcionalidades. Isso torna o código mais modular e fácil de compreender.
- Minimização de Dependências: Os pacotes devem depender de outros pacotes apenas quando realmente necessário. Em Go, podemos cair em
circular dependency
se não tomarmos cuidado com as dependências. - Estrutura Padronizada: Utilizar uma estrutura previsível facilita o entendimento e a navegação pelo código, especialmente em equipes.
- Testabilidade: Organizar o código de forma a facilitar a criação de testes unitários e de integração.
- Inversão de Dependência: Usar interfaces para abstrair dependências, especialmente pacotes externos, mantendo o código mais flexível e desacoplado.
- Independência de Pacotes de Terceiros: Evite dependências diretas de pacotes externos, utilizando interfaces e sempre que possível, criando wrappers para interagir com essas dependências.
cmd
Vamos começar pela pasta cmd
, onde fica o código principal da aplicação, o arquivo main.go
. Esse arquivo é responsável por integrar todas as dependências do projeto e gerar o executável. Podemos criar subpastas dentro de cmd para cada aplicação executável do projeto, como api
e worker
, organizando os pontos de entrada de cada serviço.
├── cmd
│ ├── api
│ │ └── main.go
│ └── worker
│ └── main.go
domain
Dominio (domain types)
A pasta domain
é onde ficam os tipos de domínio da aplicação. Tipos de domínio são estruturas de dados que representam os conceitos principais do negócio da aplicação. Por exemplo, em um sistema de vendas, os tipos de domínio poderiam incluir Order
, Product
, Customer
. Esses tipos refletem as entidades principais da aplicação.
Então, vamos criar os tipos de domínio para o nosso projeto da seguinte forma:
├── domain
│ ├── product.go
│ └── order.go
O código dentro de cada arquivo seria algo assim:
// domain/product.go
package domain
import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type Product struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
Name string `json:"name" bson:"name"`
Description string `json:"description" bson:"description"`
Price float64 `json:"price" bson:"price"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
}
// domain/order.go
package domain
import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type OrderStatus string
const (
OrderStatusPending OrderStatus = "pending"
OrderStatusApproved OrderStatus = "approved"
)
type Order struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
Customer string `json:"customer" bson:"customer"`
Products []Product `json:"products" bson:"products"`
Total float64 `json:"total" bson:"total"`
Status OrderStatus `json:"status" bson:"status"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
}
internal
A pasta internal
é onde fica o código que não deve ser acessado externamente. O próprio Go impede que pacotes dentro da pasta internal possam ser acessados por outros arquivos fora do mesmo pacote main
. Eu gosto de usar essa pasta para colocar a lógica de negócio da minha aplicação, separando-a em subpastas por domínio (domain types).
├── internal
│ ├── order
│ │ ├── dto.go
│ │ ├── service.go
│ │ ├── repository.go
│ │ └── handler.go
│ └── product
│ ├── dto.go
│ ├── service.go
│ ├── repository.go
│ └── handler.go
Observe que aqui é onde a lógica de negócio da aplicação é implementada. Cada tipo de domínio tem um arquivo service.go
que contém a lógica de negócio para esse tipo. O arquivo repository.go
contém a lógica de acesso a dados para esse tipo. O arquivo handler.go
contém a lógica de manipulação de solicitações HTTP para esse tipo. O arquivo dto.go
contém os tipos de dados de transferência que são usados para representar os dados que são passados entre as camadas.
DTO
Apesar de termos a pasta domain com a estrutura de dados que vamos armazenas no banco de dados, pode ser que precisemos de uma estrutura de dados diferente para representar os dados que queremos tratar como editaveis na nossa API. Para isso podemos usar o arquivo dto.go
para criar essas estruturas. No caso de um endpoint PUT
ou POST
eu adiciono o sufixo Input
, então para uma função de criação de pedidos, eu teria algo assim:
// internal/order/dto.go
type CreateInput struct {
Customer string `json:"customer" validate:"required"`
Total float64 `json:"total" validate:"required"`
Status string `json:"status" validate:"required,oneof=pending approved"`
}
No caso de um endpoint GET
para aqueles que vão ter query params, eu adiciono o sufixo Params
, então para uma funcão de listagem de pedidos, eu teria algo assim:
// internal/order/dto.go
type ListParams struct {
Customer string `json:"customer"`
}
Para definir o payload de resposta dos endpoints, você pode criar uma nova struct específica para o retorno. Nesse caso, eu utilizo o sufixo Response
.
// internal/order/dto.go
type CreateResponse struct {
Customer string `json:"customer"`
Products []Product `json:"products"`
Total float64 `json:"total"`
Status OrderStatus `json:"status"`
}
Service
O service é onde a lógica de negócio é implementada. Ele é responsável por orquestrar as chamadas ao repositório, validar os dados de entrada e saída, e executar a lógica de negócio
// internal/order/service.go
package order
import (
"context"
"github.com/cmparrela/ddev-pkg-oriented-design/domain"
"github.com/go-playground/validator/v10"
)
type Service interface {
Create(ctx context.Context, input CreateInput) (CreateResponse, error)
}
type service struct {
validator validator.Validate
repository Repository
}
func NewService(validator validator.Validate, repository Repository) Service {
return &service{
validator: validator,
repository: repository,
}
}
func (s *service) Create(ctx context.Context, input CreateInput) (*domain.Order, error) {
err := s.validator.Struct(input)
if err != nil {
return nil, err
}
order := domain.Order{
Customer: input.Customer,
Total: input.Total,
Status: domain.OrderStatus(input.Status),
}
order, err := s.repository.Create(ctx, order)
if err != nil {
return nil, err
}
return order, nil
}
config
A pasta config
é onde ficam os arquivos de configuração da aplicação.
├── config
│ └── config.go
Exemplo de arquivo de configuração:
//config/config.go
package config
import (
"github.com/kelseyhightower/envconfig"
)
type Config struct {
MongoURI string `envconfig:"mongo_uri" default:"mongodb://localhost:27017"`
MongoDatabaseName string `envconfig:"mongo_database_name" default:"app-example"`
}
func New() (cfg Config, err error) {
err = envconfig.Process("", &cfg)
return
}
pkg
A pasta pkg
normalmente é utilizada para colocar lib interna, wrappers e código que pode ser reutilizado em outros projetos, por exemplo, um pacote de log, um pacote de conexão com banco de dados, etc
├── pkg
│ ├── httpclient
│ │ └── client.go
│ ├── mongodb
│ │ └── mongo.go
│ ├── validator
│ │ └── validator.go
│ └── logger
│ └── logger.go
Estrutura completa
Aqui está a estrutura completa do projeto:
├── cmd
│ ├── api
│ │ └── main.go
│ ├── worker
│ │ └── main.go
│
├── domain
│ ├── product.go
│ ├── order.go
│
├── internal
│ ├── order
│ │ ├── dto.go
│ │ ├── service.go
│ │ ├── repository.go
│ │ └── handler.go
│ ├── product
│ │ ├── dto.go
│ │ ├── service.go
│ │ ├── repository.go
│ │ └── handler.go
│
├── config
│ ├── config.go
│
├── pkg
│ ├── httpclient
│ │ └── client.go
│ ├── mongodb
│ │ └── mongo.go
│ ├── validator
│ │ └── validator.go
│ ├── logger
│ │ └── logger.go