DEV Community

Cover image for Implementando Clean Architecture com Golang
Vinícius Boscardin
Vinícius Boscardin

Posted on

Implementando Clean Architecture com Golang

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
  • core/
    • domain/
      • product.go
    • dto/
      • product.go
  • 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
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

E também altere o arquivo database/migrations/000001.create_product_table.down.sql.

DROP TABLE IF EXISTS product;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"`
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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,
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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

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,
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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

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,
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

E a estrutura final ficou:

Image description

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
Enter fullscreen mode Exit fullscreen mode

Com isso vai aparecer algo assim no terminal:

Image description

Testando, 1..2..3.. Teste som!

Para criar um produto basta mandar um JSON em uma request POST na URL: localhost:port/product

Image description
Para listar os produtos com paginação é so mandar um GET maroto na URL localhost:port/product

Image description

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

Repositório

Top comments (4)

Collapse
 
luankkobs profile image
Luan Kobs

Muito obrigado por isso! Me ajudou demais

Collapse
 
laplace profile image
Fernando Costa

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.

Collapse
 
mabebrahimi profile image
Ali Ebrahimi

I guess a good article
where is the English version?

Collapse
 
booscaaa profile image
Vinícius Boscardin

Hi @mabebrahimi
As soon as I finish the clean architecture series with golang I will post a complete version in English.