从理论到实践:Go 项目中的整洁架构设计

前言

你维护的 Go 项目代码架构是什么样子的?六边形架构?还是洋葱架构?亦或者是 DDD?无论项目采用的是什么架构,核心目标都应是一致的:使代码能够易于理解、测试和维护。

本文将从 Bob 大叔的整洁架构(Clean Architecture)出发,简要解析其核心思想,并结合 go-clean-arch 仓库,深入探讨如何在 Go 项目中实现这一架构理念。

准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。

整洁架构

整洁架构(Clean Architecture)是 Bob 大叔提出的一个软件架构设计理念,旨在通过分层结构和明确的依赖规则,使软件系统更易于理解、测试和维护。其核心思想是分离关注点,确保系统中的核心业务逻辑(Use Cases)不依赖于实现细节(如框架、数据库等)。

Clean Architecture 的核心思想是 独立性

  • 独立于框架:不依赖特定的框架(如 GinGRPC 等)。框架应该是工具,而不是架构的核心。
  • 独立于 UI:用户界面可以轻松更改,而不影响系统的其他部分。例如,Web UI 可以被替换为控制台 UI,无需修改业务规则。
  • 独立于数据库:可以更换数据库(如从 MySQL 换成 MongoDB),而不影响核心业务逻辑。
  • 独立于外部工具:外部依赖(如第三方库)应该被隔离,避免其对系统核心的直接影响。

结构图

如图所示,Clean Architecture同心圆 的方式描述,其中的每一层表示不同的系统职责:

  • 核心实体(Entities
    • 位置:最内层
    • 职责:定义系统的业务规则。实体是应用中最核心的对象,具有独立的生命周期。
    • 独立性:完全独立于业务规则,只随着业务规则变化。
  • 用例(Use Cases / Service
    • 位置:紧邻实体的一层
    • 职责:实现应用的业务逻辑。定义系统中各种操作(用例)的流程,确保用户的需求被满足。
    • 作用:用例调用实体层,协调数据流向,并确定响应。
  • 接口适配器(Interface Adapters
    • 位置:更外的一层
    • 职责:负责将外部系统的数据(如 UI、数据库等)转化为内层能理解的格式,同时也用于将核心业务逻辑转换为外部系统可用的形式。 例如:将 HTTP 请求的数据转化为内部的模型(例如类或结构体),或者将用例输出的数据展示给用户。
    • 组件:包括控制器、网关(Gateways)、Presenter 等。
  • 外部框架与驱动(Frameworks & Drivers
    • 位置:最外层
    • 职责:实现与外部世界的交互,如数据库、UI、消息队列等。
    • 特点:这层依赖内层,反过来则不成立。这是系统中最容易更换的部分。

go-clean-arch 项目

go-clean-arch 是实现整洁架构(Clean Architecture)的一个 Go 示例项目。该项目有四个领域层(Domain Layer):

  • Models Layer 模型层

    • 作用:定义领域的核心数据结构,负责描述项目中的业务实体,例如 文章作者 等。
    • 对应理论层:实体层(Entities)。
    • 示例:
    go 复制代码
    package domain
    
    import (
            "time"
    )
    
    type Article struct {
            ID        int64     `json:"id"`
            Title     string    `json:"title" validate:"required"`
            Content   string    `json:"content" validate:"required"`
            Author    Author    `json:"author"`
            UpdatedAt time.Time `json:"updated_at"`
            CreatedAt time.Time `json:"created_at"`
    }
  • Repository Layer 存储层

    • 作用:负责于数据源(如数据库、缓存)交互,为用例层提供统一的接口访问数据。
    • 对应理论层:外部框架与驱动层(Frameworks & Drivers)。
    • 示例:
    go 复制代码
    package mysql
    
    import (
            "context"
            "database/sql"
            "fmt"
    
            "github.com/sirupsen/logrus"
    
            "github.com/bxcodec/go-clean-arch/domain"
            "github.com/bxcodec/go-clean-arch/internal/repository"
    )
    
    type ArticleRepository struct {
            Conn *sql.DB
    }
    
    // NewArticleRepository will create an object that represent the article.Repository interface
    func NewArticleRepository(conn *sql.DB) *ArticleRepository {
            return &ArticleRepository{conn}
    }
    
    func (m *ArticleRepository) fetch(ctx context.Context, query string, args ...interface{}) (result []domain.Article, err error) {
            rows, err := m.Conn.QueryContext(ctx, query, args...)
            if err != nil {
                    logrus.Error(err)
                    return nil, err
            }
    
            defer func() {
                    errRow := rows.Close()
                    if errRow != nil {
                            logrus.Error(errRow)
                    }
            }()
    
            result = make([]domain.Article, 0)
            for rows.Next() {
                    t := domain.Article{}
                    authorID := int64(0)
                    err = rows.Scan(
                            &t.ID,
                            &t.Title,
                            &t.Content,
                            &authorID,
                            &t.UpdatedAt,
                            &t.CreatedAt,
                    )
    
                    if err != nil {
                            logrus.Error(err)
                            return nil, err
                    }
                    t.Author = domain.Author{
                            ID: authorID,
                    }
                    result = append(result, t)
            }
    
            return result, nil
    }
    
    func (m *ArticleRepository) GetByID(ctx context.Context, id int64) (res domain.Article, err error) {
            query := `SELECT id,title,content, author_id, updated_at, created_at
                                                    FROM article WHERE ID = ?`
    
            list, err := m.fetch(ctx, query, id)
            if err != nil {
                    return domain.Article{}, err
            }
    
            if len(list) > 0 {
                    res = list[0]
            } else {
                    return res, domain.ErrNotFound
            }
    
            return
    }
  • Usecase/Service Layer 用例/服务层

    • 作用:定义系统的核心应用逻辑,是领域模型和外部交互之间的桥梁。
    • 对应理论层:用例层(Use Cases / Service)。
    • 示例:
    go 复制代码
    package article
    
    import (
            "context"
            "time"
    
            "github.com/sirupsen/logrus"
            "golang.org/x/sync/errgroup"
    
            "github.com/bxcodec/go-clean-arch/domain"
    )
    
    type ArticleRepository interface {
            GetByID(ctx context.Context, id int64) (domain.Article, error)
    }
    
    type AuthorRepository interface {
            GetByID(ctx context.Context, id int64) (domain.Author, error)
    }
    
    type Service struct {
            articleRepo ArticleRepository
            authorRepo  AuthorRepository
    }
    
    func NewService(a ArticleRepository, ar AuthorRepository) *Service {
            return &Service{
                    articleRepo: a,
                    authorRepo:  ar,
            }
    }
    
    func (a *Service) GetByID(ctx context.Context, id int64) (res domain.Article, err error) {
            res, err = a.articleRepo.GetByID(ctx, id)
            if err != nil {
                    return
            }
    
            resAuthor, err := a.authorRepo.GetByID(ctx, res.Author.ID)
            if err != nil {
                    return domain.Article{}, err
            }
            res.Author = resAuthor
            return
    }
  • Delivery Layer 交付层

    • 作用:负责接收外部请求,调用用例层,并将结果返回给外部(如 HTTP 客户端或 CLI 用户)。
    • 对应理论层:接口适配器层(Interface Adapters)。
    • 示例:
    go 复制代码
    package rest
    
    import (
            "context"
            "net/http"
            "strconv"
    
            "github.com/bxcodec/go-clean-arch/domain"
    )
    
    type ResponseError struct {
            Message string `json:"message"`
    }
    
    type ArticleService interface {
            GetByID(ctx context.Context, id int64) (domain.Article, error)
    }
    
    // ArticleHandler  represent the httphandler for article
    type ArticleHandler struct {
            Service ArticleService
    }
    
    func NewArticleHandler(e *echo.Echo, svc ArticleService) {
            handler := &ArticleHandler{
                    Service: svc,
            }
            e.GET("/articles/:id", handler.GetByID)
    }
    
    func (a *ArticleHandler) GetByID(c echo.Context) error {
            idP, err := strconv.Atoi(c.Param("id"))
            if err != nil {
                    return c.JSON(http.StatusNotFound, domain.ErrNotFound.Error())
            }
    
            id := int64(idP)
            ctx := c.Request().Context()
    
            art, err := a.Service.GetByID(ctx, id)
            if err != nil {
                    return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()})
            }
    
            return c.JSON(http.StatusOK, art)
    }

go-clean-arch 项目大体的代码架构结构如下:

txt 复制代码
go-clean-arch/
├── internal/
│   ├── rest/
│   │   └── article.go           # Delivery Layer 交付层
│   ├── repository/
│   │   ├── mysql/
│   │   │   └── article.go       # Repository Layer 存储层
├── article/
│   └── service.go               # Usecase/Service Layer 用例/服务层
├── domain/
│   └── article.go               # Models Layer 模型层

go-clean-arch 项目中,各层之间的依赖关系如下:

  • Usecase/Service 层依赖 Repository 接口,但并不知道接口的实现细节。
  • Repository 层实现了接口,但它是一个外层组件,依赖于 Domain 层的实体。
  • Delivery 层(如 REST Handler)调用 Usecase/Service 层,负责将外部请求转化为业务逻辑调用。

这种设计遵循了依赖倒置原则,确保核心业务逻辑独立于外部实现细节,具有更高的可测试性和灵活性。

小结

本文结合 Bob 大叔的 整洁架构(Clean Architecture)go-clean-arch 示例项目,介绍了如何在 Go 项目中实现整洁架构。通过核心实体、用例、接口适配器和外部框架等分层结构,清晰地分离关注点,使系统的核心业务逻辑(Use Cases)与外部实现细节(如框架、数据库)解耦。

go-clean-arch 项目架构采用分层方式组织代码,各层职责分明:

  • 模型层(Domain Layer):定义核心业务实体,独立于外部实现。
  • 用例层(Usecase Layer):实现应用逻辑,协调实体与外部交互。
  • 存储层(Repository Layer):实现数据存储的具体细节。
  • 交付层(Delivery Layer):处理外部请求并将结果返回。

这只是一个示例项目,具体项目的架构设计应根据实际需求、团队开发习惯以及规范灵活调整。核心目标是保持分层原则,确保代码易于理解、测试和维护,同时支持系统的长期扩展和演进。

相关推荐
cwtlw2 小时前
如何创建maven工程
java·笔记·后端·学习·maven
我命由我123452 小时前
15.Java 网络编程(网络相关概念、InetAddress、NetworkInterface、TCP 网络通信、UDP 网络通信、超时中断)
java·开发语言·网络·后端·tcp/ip·udp·java-ee
爱吃香菜---www3 小时前
Scala隐式泛型
开发语言·后端·scala
我爱写代码?3 小时前
Scala的隐式对象
开发语言·后端·scala
●VON4 小时前
go语言的成神之路-标准库篇-os标准库
linux·运维·服务器·开发语言·后端·学习·golang
一只拉古4 小时前
后端编程大师之路:在 .NET 应用中使用 ElasticSearch 和 Kibana 进行日志管理
后端·elasticsearch·架构
程序员大金5 小时前
基于SpringBoot+Vue的驾校管理系统
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
机智阳6 小时前
介绍一个InnoDB的数据页,和B+树的关系是什么?
java·数据结构·分布式·后端·b树
啊松同学6 小时前
【Spring】使用@Async注解后导致的循环依赖问题
java·后端·spring
rock——you6 小时前
django通过关联表字段进行排序并去重
数据库·后端·postgresql·django