Clean architecture é um assunto muito abordado nos últimos tempos. Mas... Como podemos estruturar uma arquitetura limpa com golang?
Primeiramente precisamos entender que clean architecture é uma especificação e não uma implementação. As implementações da arquitetura mais famosas são:
- Hexagonal
- DCI
- Screaming
- Onion
Nosso exemplo vai usar a arquitetura hexagonal, ou também chamada Ports and Adapters. Com a arquitetura em mãos precisamos agora definir o cenário da nossa aplicação e os requisitos que precisam existir para contemplar todas as funcionalidades. Vamos deixar pré fixado que a solução a ser criada será consumida pelo protocolo http com REST.
Os requisitos são:
- Criação de produtos (id, nome, preço e descrição)
- Listagem de produtos (com paginação no servidor)
Com os requisitos definidos, bora codar isso ai!
Calma, ainda não! Vamos definir quais tecnologias vamos usar, banco de dados, drivers de conexão e mais algumas bibliotecas que vão nos ajudar a criar a aplicação.
Usaremos então:
- Banco de dados:
- Libs no go
- Pgx: Conexão com o banco de dados
- Mux: Roteador de solicitação e um dispatcher para combinar as solicitações recebidas com seus respectivos manipuladores.
- Go-paginate: Criação de queries para o postgres
- Viper: Configurações para o ambiente de dev/prod
- Testify: Teste
- Pgx Mock: Mock para o pgx connection pool
- Migrate: Rodar as atualizações do nosso banco de dados
 
Crie uma pasta no local desejado com o nome clean-go/
Na pasta, no seu editor preferido, estruture o projeto:
- adapter/
- http/
- main.go
 
- postgres/
- connector.go
 
 
- http/
- core/
- domain/
- product.go
 
- dto/
- product.go
 
 
- domain/
- database
- migrations
 
Database
Instale a CLI migrate para gerar os arquivos de migrations necessários para o projeto.
migrate create -ext sql -dir database/migrations -seq create_product_table
Edite o arquivo gerado em database/migrations/000001.create_product_table.up.sql com o SQL da criação da tabela product.
CREATE TABLE product (
  id SERIAL PRIMARY KEY NOT NULL,
  name VARCHAR(50) NOT NULL,
  price FLOAT NOT NULL,
  description VARCHAR(500) NOT NULL
);
E também altere o arquivo database/migrations/000001.create_product_table.down.sql.
DROP TABLE IF EXISTS product;
Go modules
Vamos inicializar os módulos do go com o comando:
# go mod init github.com/<seu usuario>/<nome do repo>
# no meu caso:
go mod init github.com/booscaaa/clean-go
DTO (Data Transfer Object)
Vamos começar editando o arquivo core/dto/product.go e definindo o modelo de dados para a request de criação de um novo produto no servidor.
package dto
import (
    "encoding/json"
    "io"
)
// CreateProductRequest is an representation request body to create a new Product
type CreateProductRequest struct {
    Name        string  `json:"name"`
    Price       float32 `json:"price"`
    Description string  `json:"description"`
}
// FromJSONCreateProductRequest converts json body request to a CreateProductRequest struct
func FromJSONCreateProductRequest(body io.Reader) (*CreateProductRequest, error) {
    createProductRequest := CreateProductRequest{}
    if err := json.NewDecoder(body).Decode(&createProductRequest); err != nil {
        return nil, err
    }
    return &createProductRequest, nil
}
Em seguida definimos o DTO para nossas requests de paginação no arquivo core/dto/pagination.go.
package dto
import (
    "net/http"
    "strconv"
    "strings"
)
// PaginationRequestParms is an representation query string params to filter and paginate products
type PaginationRequestParms struct {
    Search       string   `json:"search"`
    Descending   []string `json:"descending"`
    Page         int      `json:"page"`
    ItemsPerPage int      `json:"itemsPerPage"`
    Sort         []string `json:"sort"`
}
// FromValuePaginationRequestParams converts query string params to a PaginationRequestParms struct
func FromValuePaginationRequestParams(request *http.Request) (*PaginationRequestParms, error) {
    page, _ := strconv.Atoi(request.FormValue("page"))
    itemsPerPage, _ := strconv.Atoi(request.FormValue("itemsPerPage"))
    paginationRequestParms := PaginationRequestParms{
        Search:       request.FormValue("search"),
        Descending:   strings.Split(request.FormValue("descending"), ","),
        Sort:         strings.Split(request.FormValue("sort"), ","),
        Page:         page,
        ItemsPerPage: itemsPerPage,
    }
    return &paginationRequestParms, nil
}
Domain
Com nosso DTO configurado podemos criar o core da nossa aplicação. Criaremos um aquivo chamado core/domain/pagination.go.
package domain
// Pagination is representation of Fetch methods returns
type Pagination[T any] struct {
    Items T     `json:"items"`
    Total int32 `json:"total"`
}
No arquivo core/domain/product.go vamos definir o modelo de dados referente a tabela product do banco e também as interfaces para implementação dos métodos, precisamos definir basicamente 3 interfaces: service, usecase e o nosso repository.
O service irá atender as requisições externas que batem na nossa api, o usecase é a nossa regra de negócio e o repository é nosso adapter do banco de dados.
package domain
import (
    "net/http"
    "github.com/boooscaaa/clean-go/core/dto"
)
// Product is entity of table product database column
type Product struct {
    ID          int32   `json:"id"`
    Name        string  `json:"name"`
    Price       float32 `json:"price"`
    Description string  `json:"description"`
}
// ProductService is a contract of http adapter layer
type ProductService interface {
    Create(response http.ResponseWriter, request *http.Request)
    Fetch(response http.ResponseWriter, request *http.Request)
}
// ProductUseCase is a contract of business rule layer
type ProductUseCase interface {
    Create(productRequest *dto.CreateProductRequest) (*Product, error)
    Fetch(paginationRequest *dto.PaginationRequestParms) (*Pagination[[]Product], error)
}
// ProductRepository is a contract of database connection adapter layer
type ProductRepository interface {
    Create(productRequest *dto.CreateProductRequest) (*Product, error)
    Fetch(paginationRequest *dto.PaginationRequestParms) (*Pagination[[]Product], error)
}
Repository
Com nosso domínio bem definido vamos começar definitivamente a implementação da nossa api. No arquivo adapter/postgres/connector.go vamos configurar a conexão com o banco de dados.
package postgres
import (
    "context"
    "fmt"
    "log"
    "os"
    "github.com/golang-migrate/migrate/v4"
    "github.com/jackc/pgconn"
    "github.com/jackc/pgx/v4"
    "github.com/jackc/pgx/v4/pgxpool"
    "github.com/spf13/viper"
    _ "github.com/golang-migrate/migrate/v4/database/pgx" //driver pgx used to run migrations
    _ "github.com/golang-migrate/migrate/v4/source/file"
)
// PoolInterface is an wraping to PgxPool to create test mocks
type PoolInterface interface {
    Close()
    Exec(ctx context.Context, sql string, arguments ...interface{}) (pgconn.CommandTag, error)
    Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error)
    QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row
    QueryFunc(
        ctx context.Context,
        sql string,
        args []interface{},
        scans []interface{},
        f func(pgx.QueryFuncRow) error,
    ) (pgconn.CommandTag, error)
    SendBatch(ctx context.Context, b *pgx.Batch) pgx.BatchResults
    Begin(ctx context.Context) (pgx.Tx, error)
    BeginFunc(ctx context.Context, f func(pgx.Tx) error) error
    BeginTxFunc(ctx context.Context, txOptions pgx.TxOptions, f func(pgx.Tx) error) error
}
// GetConnection return connection pool from postgres drive PGX
func GetConnection(context context.Context) *pgxpool.Pool {
    databaseURL := viper.GetString("database.url")
    conn, err := pgxpool.Connect(context, "postgres"+databaseURL)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err)
        os.Exit(1)
    }
    return conn
}
// RunMigrations run scripts on path database/migrations
func RunMigrations() {
    databaseURL := viper.GetString("database.url")
    m, err := migrate.New("file://database/migrations", "pgx"+databaseURL)
    if err != nil {
        log.Println(err)
    }
    if err := m.Up(); err != nil {
        log.Println(err)
    }
}
Com nosso connector pronto, vamos implementar a interface ProductRepository lá do nosso domain, lembra? Criaremos a estrutura da implementação assim:
- adapter
- postgres
- productrepository
- new.go
- create.go
- fetch.go
 
 
- productrepository
 
- postgres
No arquivo adapter/postgres/productrepository/new.go criaremos nossa vinculação com o "contrato" da interface ProductRepository.
package productrepository
import (
    "github.com/boooscaaa/clean-go/adapter/postgres"
    "github.com/boooscaaa/clean-go/core/domain"
)
type repository struct {
    db postgres.PoolInterface
}
// New returns contract implementation of ProductRepository
func New(db postgres.PoolInterface) domain.ProductRepository {
    return &repository{
        db: db,
    }
}
No arquivo adapter/postgres/productrepository/create.go criaremos a lógica que contempla o metodo Create do nosso contrato.
package productrepository
import (
    "context"
    "github.com/boooscaaa/clean-go/core/domain"
    "github.com/boooscaaa/clean-go/core/dto"
)
func (repository repository) Create(productRequest *dto.CreateProductRequest) (*domain.Product, error) {
    ctx := context.Background()
    product := domain.Product{}
    err := repository.db.QueryRow(
        ctx,
        "INSERT INTO product (name, price, description) VALUES ($1, $2, $3) returning *",
        productRequest.Name,
        productRequest.Price,
        productRequest.Description,
    ).Scan(
        &product.ID,
        &product.Name,
        &product.Price,
        &product.Description,
    )
    if err != nil {
        return nil, err
    }
    return &product, nil
}
No arquivo adapter/postgres/productrepository/fetch.go criaremos a lógica que contempla o método Fetch do nosso contrato.
package productrepository
import (
    "context"
    "github.com/boooscaaa/clean-go/core/domain"
    "github.com/boooscaaa/clean-go/core/dto"
    "github.com/booscaaa/go-paginate/paginate"
)
func (repository repository) Fetch(pagination *dto.PaginationRequestParms) (*domain.Pagination[[]domain.Product], error) {
    ctx := context.Background()
    products := []domain.Product{}
    total := int32(0)
    query, queryCount, err := paginate.Paginate("SELECT * FROM product").
        Page(pagination.Page).
        Desc(pagination.Descending).
        Sort(pagination.Sort).
        RowsPerPage(pagination.ItemsPerPage).
        SearchBy(pagination.Search, "name", "description").
        Query()
    if err != nil {
        return nil, err
    }
    {
        rows, err := repository.db.Query(
            ctx,
            *query,
        )
        if err != nil {
            return nil, err
        }
        for rows.Next() {
            product := domain.Product{}
            rows.Scan(
                &product.ID,
                &product.Name,
                &product.Price,
                &product.Description,
            )
            products = append(products, product)
        }
    }
    {
        err := repository.db.QueryRow(ctx, *queryCount).Scan(&total)
        if err != nil {
            return nil, err
        }
    }
    return &domain.Pagination[[]domain.Product]{
        Items: products,
        Total: total,
    }, nil
}
Repository pronto! :D
UseCase
Com nosso repository finalizado vamos implementar a regra de negócios da nossa aplicação. Criaremos a estrutura da implementação assim:
- core
- domain
- usecase
- productusecase
- new.go
- create.go
- fetch.go
 
 
- productusecase
 
- usecase
 
- domain
No arquivo core/domain/usecase/productusecase/new.go criaremos nossa vinculação com o "contrato" da interface ProductUseCase.
package productusecase
import "github.com/boooscaaa/clean-go/core/domain"
type usecase struct {
    repository domain.ProductRepository
}
// New returns contract implementation of ProductUseCase
func New(repository domain.ProductRepository) domain.ProductUseCase {
    return &usecase{
        repository: repository,
    }
}
No arquivo core/domain/usecase/productusecase/create.go criaremos a lógica que contempla o método Create do nosso contrato.
package productusecase
import (
    "github.com/boooscaaa/clean-go/core/domain"
    "github.com/boooscaaa/clean-go/core/dto"
)
func (usecase usecase) Create(productRequest *dto.CreateProductRequest) (*domain.Product, error) {
    product, err := usecase.repository.Create(productRequest)
    if err != nil {
        return nil, err
    }
    return product, nil
}
No arquivo core/domain/usecase/productusecase/fetch.go criaremos a lógica que contempla o método Fetch do nosso contrato.
package productusecase
import (
    "github.com/boooscaaa/clean-go/core/domain"
    "github.com/boooscaaa/clean-go/core/dto"
)
func (usecase usecase) Fetch(paginationRequest *dto.PaginationRequestParms) (*domain.Pagination[[]domain.Product], error) {
    products, err := usecase.repository.Fetch(paginationRequest)
    if err != nil {
        return nil, err
    }
    return products, nil
}
Service
Com nosso usecase finalizado vamos implementar o adapter do http para receber as requisições da aplicação. Criaremos a estrutura da implementação assim:
- adapter
- http
- productservice
- new.go
- create.go
- fetch.go
 
 
- productservice
 
- http
No arquivo adapter/http/productservice/new.go criaremos nossa vinculação com o "contrato" da interface ProductService.
package productservice
import "github.com/boooscaaa/clean-go/core/domain"
type service struct {
    usecase domain.ProductUseCase
}
// New returns contract implementation of ProductService
func New(usecase domain.ProductUseCase) domain.ProductService {
    return &service{
        usecase: usecase,
    }
}
No arquivo adapter/http/productservice/create.go criaremos a lógica que contempla o método Create do nosso contrato.
package productservice
import (
    "encoding/json"
    "net/http"
    "github.com/boooscaaa/clean-go/core/dto"
)
func (service service) Create(response http.ResponseWriter, request *http.Request) {
    productRequest, err := dto.FromJSONCreateProductRequest(request.Body)
    if err != nil {
        response.WriteHeader(500)
        response.Write([]byte(err.Error()))
        return
    }
    product, err := service.usecase.Create(productRequest)
    if err != nil {
        response.WriteHeader(500)
        response.Write([]byte(err.Error()))
        return
    }
    json.NewEncoder(response).Encode(product)
}
No arquivo adapter/http/productservice/fetch.go criaremos a lógica que contempla o método Fetch do nosso contrato.
package productservice
import (
    "encoding/json"
    "net/http"
    "github.com/boooscaaa/clean-go/core/dto"
)
func (service service) Fetch(response http.ResponseWriter, request *http.Request) {
    paginationRequest, err := dto.FromValuePaginationRequestParams(request)
    if err != nil {
        response.WriteHeader(500)
        response.Write([]byte(err.Error()))
        return
    }
    products, err := service.usecase.Fetch(paginationRequest)
    if err != nil {
        response.WriteHeader(500)
        response.Write([]byte(err.Error()))
        return
    }
    json.NewEncoder(response).Encode(products)
}
Tudo pronto! Brincadeira... Estamos quase lá, vamos configurar nossa injeção de dependências, nosso arquivo adapter/http/main.go para rodar a aplicação e o arquivo json de configurações de conexão do banco de dados.
Para configurar a injeção de dependência do nosso product vamos criar um arquivo em di/product.go.
package di
import (
    "github.com/boooscaaa/clean-go/adapter/http/productservice"
    "github.com/boooscaaa/clean-go/adapter/postgres"
    "github.com/boooscaaa/clean-go/adapter/postgres/productrepository"
    "github.com/boooscaaa/clean-go/core/domain"
    "github.com/boooscaaa/clean-go/core/usecase/productusecase"
)
// ConfigProductDI return a ProductService abstraction with dependency injection configuration
func ConfigProductDI(conn postgres.PoolInterface) domain.ProductService {
    productRepository := productrepository.New(conn)
    productUseCase := productusecase.New(productRepository)
    productService := productservice.New(productUseCase)
    return productService
}
E por fim configurar nosso arquivo adapter/http/main.go
package main
import (
    "context"
    "fmt"
    "log"
    "net/http"
    "github.com/boooscaaa/clean-go/adapter/postgres"
    "github.com/boooscaaa/clean-go/di"
    "github.com/gorilla/mux"
    "github.com/spf13/viper"
)
func init() {
    viper.SetConfigFile(`config.json`)
    err := viper.ReadInConfig()
    if err != nil {
        panic(err)
    }
}
func main() {
    ctx := context.Background()
    conn := postgres.GetConnection(ctx)
    defer conn.Close()
    postgres.RunMigrations()
    productService := di.ConfigProductDI(conn)
    router := mux.NewRouter()
    router.Handle("/product", http.HandlerFunc(productService.Create)).Methods("POST")
    router.Handle("/product", http.HandlerFunc(productService.Fetch)).Queries(
        "page", "{page}",
        "itemsPerPage", "{itemsPerPage}",
        "descending", "{descending}",
        "sort", "{sort}",
        "search", "{search}",
    ).Methods("GET")
    port := viper.GetString("server.port")
    log.Printf("LISTEN ON PORT: %v", port)
    http.ListenAndServe(fmt.Sprintf(":%v", port), router)
}
Agora só a configuração de conexão com o banco de dados e a porta que a api vai rodar no aquivo config.json na raiz do projeto:
{
  "database": {
    "url": "://postgres:postgres@localhost:5432/devtodb"
  },
  "server": {
    "port": "3000"
  }
}
E a estrutura final ficou:
Hora da verdade!
Será mesmo que o projeto vai rodar lisinho? É o que veremos.
Para executar a api basta se posicionar na raiz do projeto e rodar:
go run adapter/http/main.go
Com isso vai aparecer algo assim no terminal:
Testando, 1..2..3.. Teste som!
Para criar um produto basta mandar um JSON em uma request POST na URL: localhost:port/product

Para listar os produtos com paginação é so mandar um GET maroto na URL localhost:port/product 
Sua vez
Vai na fé! Acredito totalmente em você, independente do seu nível de conhecimento técnico, você vai criar a melhor api em GO.
Se você se deparar com problemas que não consegue resolver, sinta-se à vontade para entrar em contato. Vamos resolver isso juntos.
Onde tá os testes unitários?
Bora lá, próximo post vamos abordar isso e também mexer bastante com o coverage do Go. Vai ser muito legal! Até logo
 
 
              



 
    
Top comments (4)
Muito obrigado por isso! Me ajudou demais
Cara, parabéns pelo conteúdo ! Um conteúdo de muito valor.
Uma dúvida, no ProductUsecase e ProductRepository você passou o DTO como um ponteiro. Conseguiria me explicar o motivo ? Isso é um pouco complicado pra mim, visto que vim do javascript haha. Abraço.
I guess a good article
where is the English version?
Hi @mabebrahimi
As soon as I finish the clean architecture series with golang I will post a complete version in English.