Implementando Clean Architecture com Golang

2025-06-08

Implementando Clean Architecture com Golang

干净的建筑和节奏。 Mas... Como podemos estruturar uma arquitetura limpa com golang?

最初的要求是干净的建筑、特定的和不实施的。作为实现 arquitetura mais famosas são:

  • 六边形
  • 数据中心
  • 尖叫
  • 洋葱

我们使用六角形结构或端口和适配器作为示例。这是一个关于实际应用场景的设计,以及目前作为功能存在的必要条件。您可以通过 http com REST 协议解决问题。
要求:

  • Criação de produtos(id、名称、说明和描述)
  • Listagem de produtos(com paginação no servidor)

Com os requisitos definidos, bora codar isso ai!
冷静,ainda não! Vamos 定义了 vamos usar 的技术、银行、连接的驱动程序以及主要的数据库,以供应用程序使用。

我们的目标是:

  • 银行:
  • 自由党不行
    • Pgx: Conexão com o banco de bados
    • Mux:征求轮询员和调度员,作为征求意见的组合,与操作员相关。
    • 分页: Criação de querys para o postgres
    • Viper:开发/产品环境的配置
    • 作证: Teste
    • Pgx Mock: pgx 连接池的模拟
    • 迁移: Rodar 作为 nosso banco de bados 的 atualizações

Crie uma 面食没有本地 desejado com 或 nome clean-go/
Na 面食,没有 seu 编辑器首选,estruture 或 projeto:

  • 适配器/
    • http/
      • 主程序
    • postgres/
      • 连接器.go
  • 核/
    • 领域/
      • 产品.go
    • dto/
      • 产品.go
  • 数据库
    • 迁移

数据库

安装 CLI迁移并保存项目所需的迁移。



migrate create -ext sql -dir database/migrations -seq create_product_table


Enter fullscreen mode Exit fullscreen mode

编辑并获取database/migrations/000001.create_product_table.up.sqlSQL 数据表产品。



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

另请参阅或存档database/migrations/000001.create_product_table.down.sql



DROP TABLE IF EXISTS product;


Enter fullscreen mode Exit fullscreen mode

Go 模块

Vamos 的初始模块为 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(数据传输对象)

core/dto/product.go根据对服务商的新产品的请求,我们可以编辑或定义数据模型。



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

后续定义或 DTO 请求不保留分页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

领域

您可以使用 DTO 配置核心应用程序。 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

没有任何core/domain/product.go关于银行和银行产品的模型的定义,作为实施方法的接口,首先定义了 3 个基本接口:service, usecaseeo nosso repository
服务是作为外部 API 的必要条件提供的,或者银行的应用程序和存储库的适配器。



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

存储库

我们定义了 api 的实现。无需配置银行adapter/postgres/connector.go账户。



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 连接器很快就会实现,vamos 实现了 ProductRepository 接口,该接口属于 nosso 域吗? Criaremos 是一个实现 assim 的工具:

  • 适配器
    • PostgreSQL
      • 产品库
        • 新.go
        • 创建.go
        • fetch.go

adapter/postgres/productrepository/new.go没有任何问题与 ProductRepository 界面的“contrato”有关



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

没有任何问题adapter/postgres/productrepository/create.go需要考虑创建合同的逻辑或方法。



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

没有任何问题adapter/postgres/productrepository/fetch.go需要考虑逻辑或方法来获取合同。



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

存储库即将到来!:D

用例

Com nosso 存储库最终完成了应用程序的注册。 Criaremos 是一个实现 assim 的工具:

    • 领域
      • 用例
        • 产品用例
          • 新.go
          • 创建.go
          • fetch.go

没有任何关于core/domain/usecase/productusecase/new.goProductUseCase 接口的“contrato”问题。



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

没有任何问题core/domain/usecase/productusecase/create.go需要考虑逻辑或方法来创建合同。



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

没有任何问题core/domain/usecase/productusecase/fetch.go需要考虑逻辑或方法来获取合同。



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

服务

Com nosso 用例最终实现了适配器以作为应用程序的要求接收 HTTP。 Criaremos 是一个实现 assim 的工具:

  • 适配器
    • http
      • 产品服务
        • 新.go
        • 创建.go
        • fetch.go

adapter/http/productservice/new.go没有任何问题与 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

没有任何问题adapter/http/productservice/create.go需要考虑逻辑或方法来创建合同。



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

没有任何问题adapter/http/productservice/fetch.go需要考虑逻辑或方法来获取合同。



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

都多快点!布林卡迪拉... 是的,您可以配置依赖项,并使用 json 格式的应用程序配置银行连接adapter/http/main.go
配置依赖于该产品的注入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 arquivoadapter/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 可以配置连接到银行和 api 的端口,无需进行项目config.json



{
  "database": {
    "url": "://postgres:postgres@localhost:5432/devtodb"
  },
  "server": {
    "port": "3000"
  }
}


Enter fullscreen mode Exit fullscreen mode

E 最终结构:

图片描述

现在是真天!

Será mesmo que o projeto vai rodar lisinho? É o que veremos。
执行项目和rodar 的API 位置:



go run adapter/http/main.go


Enter fullscreen mode Exit fullscreen mode

Com isso vai aparecer algo assim 无终端:

图片描述

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

图片描述

苏阿维兹

Vai na fé! Acredito Totalmente em você,独立于技术的结合,可以通过 GO 的 api 进行编写。
如果问题没有解决,请直接联系我们。 Vamos 解析器 isso juntos。

Onde os testes unitários?

Bora lá,próximo post vamos abordar isso 和 também mexer bastante com o 覆盖 Go。 Vai ser muito 合法!阿特标志

存储库

鏂囩珷鏉ユ簮锛�https://dev.to/booscaaa/implementando-clean-architecture-com-golang-4n0a
PREV
学习一些计算机科学将使你成为更优秀(也更昂贵)的工程师
NEXT
Golang 开发人员的 Web 服务架构